small ui fixes

This commit is contained in:
d34dscene
2025-06-26 21:11:57 +02:00
parent 895c966209
commit b57da01cb9
16 changed files with 604 additions and 530 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,19 +112,33 @@ 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 != "" {
claims, err := meta.DecodeUserToken(token, app.Secret)
if err != nil { if err != nil {
return nil, connect.NewError( return nil, connect.NewError(
connect.CodeUnauthenticated, connect.CodeUnauthenticated,
fmt.Errorf("invalid token: %w", err), errors.New("invalid token"),
) )
} }
// Add claims to context
ctx = context.WithValue(ctx, AuthUserIDKey, claims.UserID) ctx = context.WithValue(ctx, AuthUserIDKey, claims.UserID)
return next(ctx, req) 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)
}
// Unauthorized ---------------------------------------------------
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthorized"))
}
}) })
} }
@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -278,6 +278,7 @@
<!-- Table --> <!-- Table -->
<div class="rounded-md border"> <div class="rounded-md border">
{#key table.getRowModel().rowsById}
<Table.Root> <Table.Root>
<Table.Header> <Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
@@ -339,6 +340,7 @@
</Table.Row> </Table.Row>
</Table.Footer> </Table.Footer>
</Table.Root> </Table.Root>
{/key}
</div> </div>
{#if table.getSelectedRowModel().rows.length > 0 && bulkActions && bulkActions.length > 0} {#if table.getSelectedRowModel().rows.length > 0 && bulkActions && bulkActions.length > 0}
<BulkActions <BulkActions

View File

@@ -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 />

View File

@@ -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) {

View File

@@ -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));
} }
}, },
{ {

View File

@@ -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()}

View File

@@ -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);