mirror of
https://github.com/MizuchiLabs/mantrae.git
synced 2025-12-19 21:30:14 -06:00
small ui fixes
This commit is contained in:
@@ -33,8 +33,11 @@ func (u *OIDCUserInfo) Validate() error {
|
|||||||
if u.Sub == "" {
|
if u.Sub == "" {
|
||||||
return errors.New("missing subject claim")
|
return errors.New("missing subject claim")
|
||||||
}
|
}
|
||||||
|
if !u.EmailVerified {
|
||||||
|
return errors.New("email not verified")
|
||||||
|
}
|
||||||
isValidEmail := strings.Contains(u.Email, "@") && len(u.Email) > 3 && len(u.Email) < 255
|
isValidEmail := strings.Contains(u.Email, "@") && len(u.Email) > 3 && len(u.Email) < 255
|
||||||
if u.Email != "" && isValidEmail {
|
if u.Email != "" && !isValidEmail {
|
||||||
return errors.New("invalid email format")
|
return errors.New("invalid email format")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ func UploadAvatar(a *config.App) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
defer r.MultipartForm.RemoveAll()
|
defer r.MultipartForm.RemoveAll()
|
||||||
|
|
||||||
|
userID := r.URL.Query().Get("user_id")
|
||||||
|
if userID == "" {
|
||||||
|
http.Error(w, "Missing user_id query parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
file, header, err := r.FormFile("file")
|
file, header, err := r.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to get uploaded file", http.StatusBadRequest)
|
http.Error(w, "Failed to get uploaded file", http.StatusBadRequest)
|
||||||
@@ -43,18 +49,12 @@ func UploadAvatar(a *config.App) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username := r.URL.Query().Get("username")
|
filename := fmt.Sprintf("avatar_%s%s", userID, filepath.Ext(header.Filename))
|
||||||
if username == "" {
|
_, err = a.Conn.GetQuery().GetUserByID(r.Context(), userID)
|
||||||
http.Error(w, "Username not provided", http.StatusBadRequest)
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Generate unique filename
|
|
||||||
filename := fmt.Sprintf(
|
|
||||||
"avatar_%s_%s%s",
|
|
||||||
username,
|
|
||||||
time.Now().UTC().Format("20060102_150405"),
|
|
||||||
filepath.Ext(header.Filename),
|
|
||||||
)
|
|
||||||
|
|
||||||
storePath, err := storage.GetBackend(r.Context(), a.SM, "uploads")
|
storePath, err := storage.GetBackend(r.Context(), a.SM, "uploads")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package middlewares
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -94,53 +93,8 @@ func Authentication(app *config.App) connect.UnaryInterceptorFunc {
|
|||||||
return next(ctx, req)
|
return next(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cookieHeader := req.Header().Get("Cookie"); cookieHeader != "" {
|
// Agent request (Bearer) -----------------------------------------
|
||||||
cookies, err := http.ParseCookie(cookieHeader)
|
if agentID := req.Header().Get(meta.HeaderAgentID); agentID != "" {
|
||||||
if err != nil {
|
|
||||||
return nil, connect.NewError(
|
|
||||||
connect.CodeUnauthenticated,
|
|
||||||
errors.New("invalid cookie"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var sessionID string
|
|
||||||
for _, cookie := range cookies {
|
|
||||||
if cookie.Name == meta.CookieName {
|
|
||||||
sessionID = cookie.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Validate sessionID as needed and inject into context
|
|
||||||
if sessionID != "" {
|
|
||||||
claims, err := meta.DecodeUserToken(sessionID, app.Secret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, connect.NewError(
|
|
||||||
connect.CodeUnauthenticated,
|
|
||||||
errors.New("invalid token"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ctx = context.WithValue(ctx, AuthUserIDKey, claims.UserID)
|
|
||||||
return next(ctx, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
authHeader := req.Header().Get("Authorization")
|
|
||||||
if authHeader == "" {
|
|
||||||
return nil, connect.NewError(
|
|
||||||
connect.CodeUnauthenticated,
|
|
||||||
fmt.Errorf("missing authorization header"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
|
||||||
if tokenString == authHeader {
|
|
||||||
return nil, connect.NewError(
|
|
||||||
connect.CodeUnauthenticated,
|
|
||||||
fmt.Errorf("invalid authorization header"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's an agent request
|
|
||||||
agentID := req.Header().Get(meta.HeaderAgentID)
|
|
||||||
if agentID != "" {
|
|
||||||
agent, err := app.Conn.GetQuery().GetAgent(ctx, agentID)
|
agent, err := app.Conn.GetQuery().GetAgent(ctx, agentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, connect.NewError(
|
return nil, connect.NewError(
|
||||||
@@ -148,7 +102,7 @@ func Authentication(app *config.App) connect.UnaryInterceptorFunc {
|
|||||||
errors.New("agent not found"),
|
errors.New("agent not found"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if agent.Token != tokenString {
|
if agent.Token != getBearerToken(req.Header()) {
|
||||||
return nil, connect.NewError(
|
return nil, connect.NewError(
|
||||||
connect.CodeUnauthenticated,
|
connect.CodeUnauthenticated,
|
||||||
errors.New("token mismatch"),
|
errors.New("token mismatch"),
|
||||||
@@ -158,18 +112,32 @@ func Authentication(app *config.App) connect.UnaryInterceptorFunc {
|
|||||||
return next(ctx, req)
|
return next(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and validate the token
|
// User request (Cookie/Bearer) -----------------------------------
|
||||||
claims, err := meta.DecodeUserToken(tokenString, app.Secret)
|
if token := getCookieToken(req.Header()); token != "" {
|
||||||
if err != nil {
|
claims, err := meta.DecodeUserToken(token, app.Secret)
|
||||||
return nil, connect.NewError(
|
if err != nil {
|
||||||
connect.CodeUnauthenticated,
|
return nil, connect.NewError(
|
||||||
fmt.Errorf("invalid token: %w", err),
|
connect.CodeUnauthenticated,
|
||||||
)
|
errors.New("invalid token"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, AuthUserIDKey, claims.UserID)
|
||||||
|
return next(ctx, req)
|
||||||
|
}
|
||||||
|
if token := getBearerToken(req.Header()); token != "" {
|
||||||
|
claims, err := meta.DecodeUserToken(token, app.Secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, connect.NewError(
|
||||||
|
connect.CodeUnauthenticated,
|
||||||
|
errors.New("invalid token"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, AuthUserIDKey, claims.UserID)
|
||||||
|
return next(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add claims to context
|
// Unauthorized ---------------------------------------------------
|
||||||
ctx = context.WithValue(ctx, AuthUserIDKey, claims.UserID)
|
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthorized"))
|
||||||
return next(ctx, req)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -185,6 +153,32 @@ func isPublicEndpoint(procedure string) bool {
|
|||||||
return publicEndpoints[procedure]
|
return publicEndpoints[procedure]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBearerToken(header http.Header) string {
|
||||||
|
authHeader := header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCookieToken(header http.Header) string {
|
||||||
|
cookieHeader := header.Get("Cookie")
|
||||||
|
if cookieHeader == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cookies, err := http.ParseCookie(cookieHeader)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var token string
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
if cookie.Name == meta.CookieName {
|
||||||
|
token = cookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
func GetUserIDFromContext(ctx context.Context) (string, bool) {
|
func GetUserIDFromContext(ctx context.Context) (string, bool) {
|
||||||
id, ok := ctx.Value(AuthUserIDKey).(string)
|
id, ok := ctx.Value(AuthUserIDKey).(string)
|
||||||
return id, ok
|
return id, ok
|
||||||
|
|||||||
@@ -30,68 +30,58 @@ func (rec *statusRecorder) Flush() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log middleware to log HTTP requests
|
// Logger middleware to log HTTP requests
|
||||||
func (h *MiddlewareHandler) Logger(next http.Handler) http.Handler {
|
func (h *MiddlewareHandler) Logger(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
if strings.HasPrefix(r.URL.Path, "/_app/") || r.URL.Path == "/favicon.ico" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
// Capture the response status code
|
|
||||||
recorder := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK}
|
|
||||||
|
|
||||||
// Serve the request
|
|
||||||
next.ServeHTTP(recorder, r)
|
|
||||||
duration := time.Since(start)
|
|
||||||
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/_app/") {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the request details
|
start := time.Now()
|
||||||
status := recorder.statusCode
|
rec := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK}
|
||||||
|
next.ServeHTTP(rec, r)
|
||||||
msg := "HTTP request"
|
duration := time.Since(start)
|
||||||
fields := []any{
|
|
||||||
"method", r.Method,
|
|
||||||
"url", r.URL.Path,
|
|
||||||
"status", status,
|
|
||||||
"protocol", r.Proto,
|
|
||||||
"duration_ms", duration.Milliseconds(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
level := slog.LevelDebug
|
||||||
switch {
|
switch {
|
||||||
case status >= 500:
|
case rec.statusCode >= 500:
|
||||||
slog.Error(msg, fields...)
|
level = slog.LevelError
|
||||||
case status >= 400:
|
case rec.statusCode >= 400:
|
||||||
slog.Warn(msg, fields...)
|
level = slog.LevelWarn
|
||||||
default:
|
|
||||||
slog.Info(msg, fields...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slog.Log(r.Context(), level, "http_request",
|
||||||
|
slog.String("method", r.Method),
|
||||||
|
slog.String("url", r.URL.Path),
|
||||||
|
slog.Int("status", rec.statusCode),
|
||||||
|
slog.String("protocol", r.Proto),
|
||||||
|
slog.Int64("duration_ms", duration.Milliseconds()),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Logging() connect.UnaryInterceptorFunc {
|
func Logging() connect.UnaryInterceptorFunc {
|
||||||
return func(next connect.UnaryFunc) connect.UnaryFunc {
|
return func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||||
|
if req.Spec().Procedure == "HealthCheck" {
|
||||||
|
return next(ctx, req)
|
||||||
|
}
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
resp, err := next(ctx, req)
|
resp, err := next(ctx, req)
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
if req.Spec().Procedure == "HealthCheck" {
|
logger := slog.With(
|
||||||
return resp, err
|
slog.String("method", req.Spec().Procedure),
|
||||||
}
|
slog.String("peer", req.Peer().Addr),
|
||||||
|
slog.String("protocol", req.Peer().Protocol),
|
||||||
msg := "RPC call"
|
slog.Int64("duration_ms", duration.Milliseconds()),
|
||||||
fields := []any{
|
)
|
||||||
"method", req.Spec().Procedure,
|
|
||||||
"peer", req.Peer().Addr,
|
|
||||||
"protocol", req.Peer().Protocol,
|
|
||||||
"duration_ms", duration.Milliseconds(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error(msg, append(fields, "error", err)...)
|
logger.With(slog.String("error", err.Error())).Error("rpc_call")
|
||||||
} else {
|
} else {
|
||||||
slog.Debug(msg, fields...)
|
logger.Debug("rpc_call")
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, err
|
return resp, err
|
||||||
|
|||||||
@@ -139,13 +139,6 @@ func (s *Server) registerServices() {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static files
|
|
||||||
staticContent, err := fs.Sub(web.StaticFS, "build")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
s.mux.Handle("/", http.FileServer(http.FS(staticContent)))
|
|
||||||
|
|
||||||
serviceNames := []string{
|
serviceNames := []string{
|
||||||
mantraev1connect.ProfileServiceName,
|
mantraev1connect.ProfileServiceName,
|
||||||
mantraev1connect.UserServiceName,
|
mantraev1connect.UserServiceName,
|
||||||
@@ -167,6 +160,15 @@ func (s *Server) registerServices() {
|
|||||||
s.mux.Handle(grpcreflect.NewHandlerV1(reflector))
|
s.mux.Handle(grpcreflect.NewHandlerV1(reflector))
|
||||||
s.mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector))
|
s.mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector))
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
staticContent, err := fs.Sub(web.StaticFS, "build")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
uploadsContent := http.FileServer(http.Dir("./data/uploads"))
|
||||||
|
s.mux.Handle("/", http.FileServer(http.FS(staticContent)))
|
||||||
|
s.mux.Handle("/uploads/", http.StripPrefix("/uploads/", uploadsContent))
|
||||||
|
|
||||||
// Serve OpenAPI specs file
|
// Serve OpenAPI specs file
|
||||||
s.mux.HandleFunc("/openapi.yaml", func(w http.ResponseWriter, r *http.Request) {
|
s.mux.HandleFunc("/openapi.yaml", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeFile(w, r, "proto/gen/openapi/openapi.yaml")
|
http.ServeFile(w, r, "proto/gen/openapi/openapi.yaml")
|
||||||
|
|||||||
@@ -107,12 +107,22 @@ func (h *Handler) handleJSON(r slog.Record) error {
|
|||||||
clear(attrs)
|
clear(attrs)
|
||||||
defer h.attrsPool.Put(attrs)
|
defer h.attrsPool.Put(attrs)
|
||||||
|
|
||||||
|
// Apply baseAttrs first
|
||||||
|
for _, a := range h.baseAttrs {
|
||||||
|
val := a.Value.Any()
|
||||||
|
if errVal, ok := val.(error); ok {
|
||||||
|
attrs[a.Key] = errVal.Error()
|
||||||
|
} else {
|
||||||
|
attrs[a.Key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add standard fields
|
// Add standard fields
|
||||||
attrs["time"] = r.Time.Format(time.RFC3339)
|
attrs["time"] = r.Time.Format(time.RFC3339)
|
||||||
attrs["level"] = r.Level.String()
|
attrs["level"] = r.Level.String()
|
||||||
attrs["msg"] = r.Message
|
attrs["msg"] = r.Message
|
||||||
|
|
||||||
// Add custom attributes
|
// Add record attrs
|
||||||
r.Attrs(func(a slog.Attr) bool {
|
r.Attrs(func(a slog.Attr) bool {
|
||||||
val := a.Value.Any()
|
val := a.Value.Any()
|
||||||
if errVal, ok := val.(error); ok {
|
if errVal, ok := val.(error); ok {
|
||||||
@@ -171,6 +181,15 @@ func (h *Handler) handleText(r slog.Record) error {
|
|||||||
clear(attrs)
|
clear(attrs)
|
||||||
defer h.attrsPool.Put(attrs)
|
defer h.attrsPool.Put(attrs)
|
||||||
|
|
||||||
|
for _, a := range h.baseAttrs {
|
||||||
|
val := a.Value.Any()
|
||||||
|
if errVal, ok := val.(error); ok {
|
||||||
|
attrs[a.Key] = errVal.Error()
|
||||||
|
} else {
|
||||||
|
attrs[a.Key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
r.Attrs(func(a slog.Attr) bool {
|
r.Attrs(func(a slog.Attr) bool {
|
||||||
if a.Key == slog.TimeKey || a.Key == slog.LevelKey || a.Key == slog.MessageKey {
|
if a.Key == slog.TimeKey || a.Key == slog.LevelKey || a.Key == slog.MessageKey {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -18,30 +18,30 @@
|
|||||||
"@internationalized/date": "^3.8.2",
|
"@internationalized/date": "^3.8.2",
|
||||||
"@lucide/svelte": "^0.515.0",
|
"@lucide/svelte": "^0.515.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.8",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@sveltejs/kit": "^2.22.0",
|
"@sveltejs/kit": "^2.22.2",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/node": "^22.15.32",
|
"@types/node": "^22.15.33",
|
||||||
"bits-ui": "2.8.2",
|
"bits-ui": "2.8.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-svelte": "^3.9.3",
|
"eslint-plugin-svelte": "^3.10.0",
|
||||||
"formsnap": "^2.0.1",
|
"formsnap": "^2.0.1",
|
||||||
"globals": "^16.2.0",
|
"globals": "^16.2.0",
|
||||||
"mode-watcher": "^1.0.8",
|
"mode-watcher": "^1.0.8",
|
||||||
"prettier": "^3.6.0",
|
"prettier": "^3.6.1",
|
||||||
"prettier-plugin-svelte": "^3.4.0",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.13",
|
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||||
"svelte": "^5.34.7",
|
"svelte": "^5.34.8",
|
||||||
"svelte-check": "^4.2.2",
|
"svelte-check": "^4.2.2",
|
||||||
"svelte-highlight": "^7.8.3",
|
"svelte-highlight": "^7.8.3",
|
||||||
"svelte-sonner": "^1.0.5",
|
"svelte-sonner": "^1.0.5",
|
||||||
"sveltekit-superforms": "^2.27.0",
|
"sveltekit-superforms": "^2.27.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^1.0.0",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.11",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.35.0",
|
"typescript-eslint": "^8.35.0",
|
||||||
|
|||||||
564
web/pnpm-lock.yaml
generated
564
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -90,20 +90,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog.Root bind:open>
|
<Dialog.Root bind:open>
|
||||||
<Dialog.Content class="no-scrollbar max-h-[95vh] w-[425px] overflow-y-auto">
|
<Dialog.Content class="no-scrollbar max-h-[95vh] w-[500px] overflow-y-auto">
|
||||||
<Dialog.Header class="flex flex-row items-center justify-between">
|
<Dialog.Header>
|
||||||
<div>
|
<Dialog.Title>{item?.id ? 'Edit' : 'Add'} DNS Provider</Dialog.Title>
|
||||||
<Dialog.Title>{item?.id ? 'Edit' : 'Add'} DNS Provider</Dialog.Title>
|
<Dialog.Description>Setup dns provider for automated dns records</Dialog.Description>
|
||||||
<Dialog.Description>Setup dns provider for automated dns records</Dialog.Description>
|
|
||||||
</div>
|
|
||||||
<div class="mr-4 flex items-center gap-2">
|
|
||||||
<Label for="default">Default</Label>
|
|
||||||
<Switch
|
|
||||||
id="default"
|
|
||||||
checked={item.isActive}
|
|
||||||
onCheckedChange={(value) => (item.isActive = value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<form onsubmit={handleSubmit} class="flex flex-col gap-4">
|
<form onsubmit={handleSubmit} class="flex flex-col gap-4">
|
||||||
@@ -135,7 +125,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-2 py-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<Label for="autoUpdate" class="flex flex-row items-center gap-1 text-sm font-medium">
|
||||||
|
Set as default
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<CircleHelp size={16} />
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content align="start" class="w-64">
|
||||||
|
<p>
|
||||||
|
If enabled, this DNS provider will be used as the default DNS provider for all
|
||||||
|
newly created routers.
|
||||||
|
</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
</Label>
|
||||||
|
<Tabs.Root
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
value={item.isActive ? 'on' : 'off'}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (item.isActive === undefined) item.isActive = value === 'on';
|
||||||
|
else item.isActive = value === 'on';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex justify-end" transition:slide={{ duration: 200 }}>
|
||||||
|
<Tabs.List class="h-8">
|
||||||
|
<Tabs.Trigger value="on" class="px-2 py-0.5 font-bold">On</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="off" class="px-2 py-0.5 font-bold">Off</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
</div>
|
||||||
|
</Tabs.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if item.type === DnsProviderType.CLOUDFLARE}
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<Label for="autoUpdate" class="flex flex-row items-center gap-1 text-sm font-medium">
|
||||||
|
Cloudflare Proxy
|
||||||
|
</Label>
|
||||||
|
<Tabs.Root
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
value={item.config?.proxied ? 'on' : 'off'}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (item.config === undefined) item.config = {} as DnsProviderConfig;
|
||||||
|
item.config.proxied = value === 'on';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex justify-end" transition:slide={{ duration: 200 }}>
|
||||||
|
<Tabs.List class="h-8">
|
||||||
|
<Tabs.Trigger value="on" class="px-2 py-0.5 font-bold">On</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="off" class="px-2 py-0.5 font-bold">Off</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
</div>
|
||||||
|
</Tabs.Root>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
<Label for="autoUpdate" class="flex flex-row items-center gap-1 text-sm font-medium">
|
<Label for="autoUpdate" class="flex flex-row items-center gap-1 text-sm font-medium">
|
||||||
Auto Update IP
|
Auto Update IP
|
||||||
<Tooltip.Provider>
|
<Tooltip.Provider>
|
||||||
@@ -249,19 +296,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if item.type === DnsProviderType.CLOUDFLARE}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Label for="proxied">Proxied?</Label>
|
|
||||||
<Switch
|
|
||||||
checked={item.config?.proxied}
|
|
||||||
onCheckedChange={(value) => {
|
|
||||||
if (item.config === undefined) item.config = {} as DnsProviderConfig;
|
|
||||||
item.config.proxied = value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if item.type === DnsProviderType.TECHNITIUM}
|
{#if item.type === DnsProviderType.TECHNITIUM}
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<Label for="zoneType">Zone Type</Label>
|
<Label for="zoneType">Zone Type</Label>
|
||||||
|
|||||||
@@ -278,67 +278,69 @@
|
|||||||
|
|
||||||
<!-- Table -->
|
<!-- Table -->
|
||||||
<div class="rounded-md border">
|
<div class="rounded-md border">
|
||||||
<Table.Root>
|
{#key table.getRowModel().rowsById}
|
||||||
<Table.Header>
|
<Table.Root>
|
||||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
<Table.Header>
|
||||||
<Table.Row>
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||||
{#each headerGroup.headers as header (header.id)}
|
<Table.Row>
|
||||||
<Table.Head>
|
{#each headerGroup.headers as header (header.id)}
|
||||||
{#if !header.isPlaceholder}
|
<Table.Head>
|
||||||
<div class="flex items-center">
|
{#if !header.isPlaceholder}
|
||||||
<Button
|
<div class="flex items-center">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
class="-ml-3 h-8 data-[sortable=false]:cursor-default"
|
size="sm"
|
||||||
data-sortable={header.column.getCanSort()}
|
class="-ml-3 h-8 data-[sortable=false]:cursor-default"
|
||||||
onclick={() => header.column.toggleSorting()}
|
data-sortable={header.column.getCanSort()}
|
||||||
>
|
onclick={() => header.column.toggleSorting()}
|
||||||
<FlexRender
|
>
|
||||||
content={header.column.columnDef.header}
|
<FlexRender
|
||||||
context={header.getContext()}
|
content={header.column.columnDef.header}
|
||||||
/>
|
context={header.getContext()}
|
||||||
{#if header.column.getCanSort()}
|
/>
|
||||||
{#if header.column.getIsSorted() === 'asc'}
|
{#if header.column.getCanSort()}
|
||||||
<ArrowDown />
|
{#if header.column.getIsSorted() === 'asc'}
|
||||||
{:else if header.column.getIsSorted() === 'desc'}
|
<ArrowDown />
|
||||||
<ArrowUp />
|
{:else if header.column.getIsSorted() === 'desc'}
|
||||||
|
<ArrowUp />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</Table.Head>
|
||||||
</Table.Head>
|
{/each}
|
||||||
{/each}
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{#each table.getRowModel().rows as row (row.id)}
|
||||||
|
<Table.Row
|
||||||
|
data-state={row.getIsSelected() && 'selected'}
|
||||||
|
class={computeRowClasses(row.original)}
|
||||||
|
>
|
||||||
|
{#each row.getVisibleCells() as cell (cell.id)}
|
||||||
|
<Table.Cell>
|
||||||
|
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||||
|
</Table.Cell>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
{:else}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={columns.length} class="h-24 text-center">No results.</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
</Table.Body>
|
||||||
|
<Table.Footer>
|
||||||
|
<Table.Row class="border-t">
|
||||||
|
<Table.Cell colspan={columns.length}>Total</Table.Cell>
|
||||||
|
<Table.Cell class="mr-4 text-right">
|
||||||
|
{table.getPaginationRowModel().rows.length}
|
||||||
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{/each}
|
</Table.Footer>
|
||||||
</Table.Header>
|
</Table.Root>
|
||||||
<Table.Body>
|
{/key}
|
||||||
{#each table.getRowModel().rows as row (row.id)}
|
|
||||||
<Table.Row
|
|
||||||
data-state={row.getIsSelected() && 'selected'}
|
|
||||||
class={computeRowClasses(row.original)}
|
|
||||||
>
|
|
||||||
{#each row.getVisibleCells() as cell (cell.id)}
|
|
||||||
<Table.Cell>
|
|
||||||
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
|
||||||
</Table.Cell>
|
|
||||||
{/each}
|
|
||||||
</Table.Row>
|
|
||||||
{:else}
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell colspan={columns.length} class="h-24 text-center">No results.</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
{/each}
|
|
||||||
</Table.Body>
|
|
||||||
<Table.Footer>
|
|
||||||
<Table.Row class="border-t">
|
|
||||||
<Table.Cell colspan={columns.length}>Total</Table.Cell>
|
|
||||||
<Table.Cell class="mr-4 text-right">
|
|
||||||
{table.getPaginationRowModel().rows.length}
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Footer>
|
|
||||||
</Table.Root>
|
|
||||||
</div>
|
</div>
|
||||||
{#if table.getSelectedRowModel().rows.length > 0 && bulkActions && bulkActions.length > 0}
|
{#if table.getSelectedRowModel().rows.length > 0 && bulkActions && bulkActions.length > 0}
|
||||||
<BulkActions
|
<BulkActions
|
||||||
|
|||||||
@@ -10,21 +10,11 @@
|
|||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import { user } from '$lib/stores/user';
|
import { user } from '$lib/stores/user';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { profile } from '$lib/stores/profile';
|
|
||||||
import { profileClient } from '$lib/api';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: import('svelte').Snippet;
|
children?: import('svelte').Snippet;
|
||||||
}
|
}
|
||||||
let { children }: Props = $props();
|
let { children }: Props = $props();
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (user.isLoggedIn() && !profile.id) {
|
|
||||||
const response = await profileClient.listProfiles({});
|
|
||||||
profile.value = response.profiles[0];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { profile } from "$lib/stores/profile";
|
|||||||
import { user } from "$lib/stores/user";
|
import { user } from "$lib/stores/user";
|
||||||
|
|
||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
export const prerender = true;
|
export const prerender = false;
|
||||||
export const trailingSlash = "always";
|
export const trailingSlash = "always";
|
||||||
|
|
||||||
const isPublicRoute = (path: string) => {
|
const isPublicRoute = (path: string) => {
|
||||||
@@ -15,17 +15,18 @@ const isPublicRoute = (path: string) => {
|
|||||||
export const load: LayoutLoad = async ({ url }) => {
|
export const load: LayoutLoad = async ({ url }) => {
|
||||||
const currentPath = url.pathname;
|
const currentPath = url.pathname;
|
||||||
const isPublic = isPublicRoute(currentPath);
|
const isPublic = isPublicRoute(currentPath);
|
||||||
|
// Check if cookie is set
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const verified = await userClient.verifyJWT({});
|
const resUser = await userClient.verifyJWT({});
|
||||||
|
|
||||||
if (verified.user) {
|
if (resUser.user) {
|
||||||
user.value = verified.user;
|
user.value = resUser.user;
|
||||||
|
|
||||||
// Update profile if not set
|
// Update profile if not set
|
||||||
if (!profile.id) {
|
if (!profile.id) {
|
||||||
const response = await profileClient.listProfiles({});
|
const resProfile = await profileClient.listProfiles({});
|
||||||
profile.value = response.profiles[0];
|
profile.value = resProfile.profiles[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPublic) {
|
if (isPublic) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { dnsClient } from '$lib/api';
|
import { dnsClient, utilClient } from '$lib/api';
|
||||||
import DNSModal from '$lib/components/modals/dns.svelte';
|
import DNSModal from '$lib/components/modals/dns.svelte';
|
||||||
import ColumnBadge from '$lib/components/tables/ColumnBadge.svelte';
|
import ColumnBadge from '$lib/components/tables/ColumnBadge.svelte';
|
||||||
import ColumnCheck from '$lib/components/tables/ColumnCheck.svelte';
|
import ColumnCheck from '$lib/components/tables/ColumnCheck.svelte';
|
||||||
@@ -8,8 +8,7 @@
|
|||||||
import type { BulkAction } from '$lib/components/tables/types';
|
import type { BulkAction } from '$lib/components/tables/types';
|
||||||
import { renderComponent } from '$lib/components/ui/data-table';
|
import { renderComponent } from '$lib/components/ui/data-table';
|
||||||
import { DnsProviderType, type DnsProvider } from '$lib/gen/mantrae/v1/dns_provider_pb';
|
import { DnsProviderType, type DnsProvider } from '$lib/gen/mantrae/v1/dns_provider_pb';
|
||||||
import { DateFormat, pageIndex, pageSize } from '$lib/stores/common';
|
import { pageIndex, pageSize } from '$lib/stores/common';
|
||||||
import { timestampDate, type Timestamp } from '@bufbuild/protobuf/wkt';
|
|
||||||
import { ConnectError } from '@connectrpc/connect';
|
import { ConnectError } from '@connectrpc/connect';
|
||||||
import { Globe, Pencil, Trash } from '@lucide/svelte';
|
import { Globe, Pencil, Trash } from '@lucide/svelte';
|
||||||
import type { ColumnDef, PaginationState } from '@tanstack/table-core';
|
import type { ColumnDef, PaginationState } from '@tanstack/table-core';
|
||||||
@@ -56,6 +55,29 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'IP Address',
|
||||||
|
accessorKey: 'config.ip',
|
||||||
|
id: 'ip',
|
||||||
|
enableSorting: true,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
if (row.original.config?.autoUpdate) {
|
||||||
|
// utilClient.getPublicIP({}).then((res) => {
|
||||||
|
return renderComponent(ColumnBadge, {
|
||||||
|
label: 'auto',
|
||||||
|
variant: 'secondary',
|
||||||
|
class: 'hover:cursor-pointer'
|
||||||
|
});
|
||||||
|
// });
|
||||||
|
} else {
|
||||||
|
let ip = row.getValue('ip') as string;
|
||||||
|
return renderComponent(ColumnBadge, {
|
||||||
|
label: ip,
|
||||||
|
class: 'hover:cursor-pointer'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Default',
|
header: 'Default',
|
||||||
accessorKey: 'isActive',
|
accessorKey: 'isActive',
|
||||||
@@ -66,21 +88,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Created',
|
header: 'Proxied',
|
||||||
accessorKey: 'createdAt',
|
accessorKey: 'config.proxied',
|
||||||
|
id: 'proxied',
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const date = row.getValue('createdAt') as Timestamp;
|
let checked = row.getValue('proxied') as boolean;
|
||||||
return DateFormat.format(timestampDate(date));
|
return renderComponent(ColumnCheck, { checked: checked });
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Updated',
|
|
||||||
accessorKey: 'updatedAt',
|
|
||||||
enableSorting: true,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const date = row.getValue('updatedAt') as Timestamp;
|
|
||||||
return DateFormat.format(timestampDate(date));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -64,10 +64,6 @@
|
|||||||
toast.error('Failed to login', { description: e.message });
|
toast.error('Failed to login', { description: e.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleOIDCLogin = () => {
|
|
||||||
// window.location.href = '/oidc/login';
|
|
||||||
// };
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !user.isLoggedIn()}
|
{#if !user.isLoggedIn()}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import { pageIndex, pageSize } from '$lib/stores/common';
|
import { pageIndex, pageSize } from '$lib/stores/common';
|
||||||
import { profile } from '$lib/stores/profile';
|
import { profile } from '$lib/stores/profile';
|
||||||
import { ConnectError } from '@connectrpc/connect';
|
import { ConnectError } from '@connectrpc/connect';
|
||||||
import { Bot, CircleSlash, Pencil, Route, Trash } from '@lucide/svelte';
|
import { Bot, CircleCheck, CircleSlash, Pencil, Route, Trash } from '@lucide/svelte';
|
||||||
import type { ColumnDef, PaginationState } from '@tanstack/table-core';
|
import type { ColumnDef, PaginationState } from '@tanstack/table-core';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
@@ -183,19 +183,26 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const bulkActions: BulkAction<Router>[] = [
|
const bulkActions: BulkAction<Router>[] = [
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
label: 'Enable',
|
||||||
|
icon: CircleCheck,
|
||||||
|
variant: 'outline',
|
||||||
|
onClick: (e) => bulk(e, 'enable')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
label: 'Disable',
|
label: 'Disable',
|
||||||
icon: CircleSlash,
|
icon: CircleSlash,
|
||||||
variant: 'outline',
|
variant: 'outline',
|
||||||
onClick: (e) => bulkDelete(e, 'disable')
|
onClick: (e) => bulk(e, 'disable')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
icon: Trash,
|
icon: Trash,
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
onClick: (e) => bulkDelete(e, 'delete')
|
onClick: (e) => bulk(e, 'delete')
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -214,7 +221,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function bulkDelete(rows: Router[], action: string) {
|
async function bulk(rows: Router[], action: string) {
|
||||||
try {
|
try {
|
||||||
const confirmed = confirm(`Are you sure you want to ${action} ${rows.length} routers?`);
|
const confirmed = confirm(`Are you sure you want to ${action} ${rows.length} routers?`);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
@@ -237,6 +244,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'enable':
|
||||||
|
for (const row of rows) {
|
||||||
|
await routerClient.updateRouter({
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
type: row.type,
|
||||||
|
config: row.config,
|
||||||
|
dnsProviders: row.dnsProviders,
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshData(pageSize.value ?? 10, pageIndex.value ?? 0);
|
await refreshData(pageSize.value ?? 10, pageIndex.value ?? 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user