From 111e3ee59c05ce6ac1ed2dc8cc6907f88498730e Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Sat, 17 Oct 2026 09:49:51 +0100 Subject: [PATCH 1/8] Add frontend/Dockerfile --- frontend/Dockerfile | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 frontend/Dockerfile diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..ed4d51e9 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,33 @@ +# Multi-stage Dockerfile for the Next.js frontend +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev + +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_PUBLIC_API_URL=http://localhost:6003/api +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_PUBLIC_API_URL=http://localhost:6003/api + +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1 + +CMD ["node", "server.js"] \ No newline at end of file From 7f4fd73876dc31555eb680fed8c01eb579759f86 Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Sat, 17 Oct 2026 09:49:55 +0100 Subject: [PATCH 2/8] Update next.config.ts - add standalone output --- frontend/next.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa308..de2d453e 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", }; -export default nextConfig; +export default nextConfig; \ No newline at end of file From be0843555233b12fcf6f729c6b58389fa8f2de07 Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Sat, 17 Oct 2026 09:49:58 +0100 Subject: [PATCH 3/8] Add frontend service to docker-compose.yml --- docker-compose.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index f48a378c..14a5498b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,6 +62,19 @@ services: - /app/node_modules command: npm run start:dev + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: assetsup_frontend + restart: unless-stopped + depends_on: + - backend + environment: + NEXT_PUBLIC_API_URL: http://localhost:6003/api + ports: + - "3001:3000" + volumes: postgres_data: - redis_data: + redis_data: \ No newline at end of file From 6826d52b51d535951cfa71ea82d5415fcef82245 Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Sat, 17 Oct 2026 09:50:23 +0100 Subject: [PATCH 4/8] Update auth.store.ts - move tokens to httpOnly cookies --- frontend/store/auth.store.ts | 58 ++++++++---------------------------- 1 file changed, 13 insertions(+), 45 deletions(-) diff --git a/frontend/store/auth.store.ts b/frontend/store/auth.store.ts index 1e2246f3..30dc3911 100644 --- a/frontend/store/auth.store.ts +++ b/frontend/store/auth.store.ts @@ -1,21 +1,17 @@ import { create } from 'zustand'; -import { AUTH_COOKIE } from '@/lib/auth-routes'; import { authApi, AuthUser, LoginPayload, RegisterPayload } from '@/lib/auth-api'; -// Cookie helpers for middleware access (middleware runs on Edge, can't read localStorage) -function setAuthCookie(token: string) { - document.cookie = `${AUTH_COOKIE}=${token}; path=/; max-age=${15 * 60}; SameSite=Lax`; -} - -function clearAuthCookie() { - document.cookie = `${AUTH_COOKIE}=; path=/; max-age=0`; -} +/** + * Token handling has moved from localStorage to httpOnly cookies. + * The backend sets `HttpOnly; Secure; SameSite=Lax` cookies for both + * access and refresh tokens. The client sends `withCredentials` on + * all requests so the browser attaches them automatically. + */ interface AuthState { user: AuthUser | null; isLoading: boolean; isAuthenticated: boolean; - login: (payload: LoginPayload) => Promise; register: (payload: RegisterPayload) => Promise; logout: () => Promise; @@ -27,63 +23,35 @@ export const useAuthStore = create((set) => ({ user: null, isLoading: false, isAuthenticated: false, - setUser: (user) => set({ user, isAuthenticated: !!user }), login: async (payload) => { set({ isLoading: true }); try { const data = await authApi.login(payload); - localStorage.setItem('accessToken', data.accessToken); - localStorage.setItem('refreshToken', data.refreshToken); - setAuthCookie(data.accessToken); set({ user: data.user, isAuthenticated: true }); - } finally { - set({ isLoading: false }); - } + } finally { set({ isLoading: false }); } }, register: async (payload) => { set({ isLoading: true }); try { const data = await authApi.register(payload); - localStorage.setItem('accessToken', data.accessToken); - localStorage.setItem('refreshToken', data.refreshToken); - setAuthCookie(data.accessToken); set({ user: data.user, isAuthenticated: true }); - } finally { - set({ isLoading: false }); - } + } finally { set({ isLoading: false }); } }, logout: async () => { - try { - await authApi.logout(); - } finally { - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); - clearAuthCookie(); - set({ user: null, isAuthenticated: false }); - } + try { await authApi.logout(); } + finally { set({ user: null, isAuthenticated: false }); } }, loadUser: async () => { set({ isLoading: true }); - const token = localStorage.getItem('accessToken'); - if (!token) { - set({ isLoading: false }); - return; - } try { const user = await authApi.me(); set({ user, isAuthenticated: true }); - } catch { - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); - clearAuthCookie(); - set({ user: null, isAuthenticated: false }); - } finally { - set({ isLoading: false }); - } + } catch { set({ user: null, isAuthenticated: false }); } + finally { set({ isLoading: false }); } }, -})); +})); \ No newline at end of file From 5570e2e8761bd5442557a075cb628f45d9a92dff Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Sat, 17 Oct 2026 09:50:26 +0100 Subject: [PATCH 5/8] Add lib/api/custom-fields.ts --- frontend/lib/api/custom-fields.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 frontend/lib/api/custom-fields.ts diff --git a/frontend/lib/api/custom-fields.ts b/frontend/lib/api/custom-fields.ts new file mode 100644 index 00000000..89d64ac0 --- /dev/null +++ b/frontend/lib/api/custom-fields.ts @@ -0,0 +1,24 @@ +import { api } from '../api'; + +export interface CustomFieldDef { + id: string; + key: string; + label: string; + type: 'text' | 'number' | 'date' | 'select' | 'boolean'; + options?: string[]; + required: boolean; + isActive: boolean; +} + +export const customFieldsApiClient = { + list: (category: string): Promise => + api.get('/custom-fields', { params: { category } }).then((r) => r.data), + create: (category: string, data: Omit): Promise => + api.post('/custom-fields', { ...data, category }).then((r) => r.data), + update: (id: string, data: Partial): Promise => + api.patch(`/custom-fields/${id}`, data).then((r) => r.data), + delete: (id: string): Promise => + api.delete(`/custom-fields/${id}`).then(() => undefined), + toggleActive: (id: string, isActive: boolean): Promise => + api.patch(`/custom-fields/${id}`, { isActive }).then((r) => r.data), +}; \ No newline at end of file From e34b8ee52a3cda09bdced915d395479e22f70852 Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Sat, 17 Oct 2026 09:50:30 +0100 Subject: [PATCH 6/8] Add lib/api/developer-settings.ts --- frontend/lib/api/developer-settings.ts | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 frontend/lib/api/developer-settings.ts diff --git a/frontend/lib/api/developer-settings.ts b/frontend/lib/api/developer-settings.ts new file mode 100644 index 00000000..3a4c388b --- /dev/null +++ b/frontend/lib/api/developer-settings.ts @@ -0,0 +1,39 @@ +import { api } from '../api'; + +export interface APIKey { + id: string; + name: string; + prefix: string; + lastUsed?: string; + isActive: boolean; + expiresAt?: string; + scopes: string[]; +} + +export interface Webhook { + id: string; + url: string; + events: string[]; + isActive: boolean; + signingSecret?: string; + recentDeliveries: { timestamp: string; event: string; status: number }[]; +} + +export const developerSettingsApiClient = { + listApiKeys: (): Promise => + api.get('/developer/api-keys').then((r) => r.data), + createApiKey: (name: string, scopes: string[]): Promise<{ key: APIKey; secret: string }> => + api.post<{ key: APIKey; secret: string }>('/developer/api-keys', { name, scopes }).then((r) => r.data), + revokeApiKey: (id: string): Promise => + api.delete(`/developer/api-keys/${id}`).then(() => undefined), + listWebhooks: (): Promise => + api.get('/developer/webhooks').then((r) => r.data), + createWebhook: (url: string, events: string[]): Promise => + api.post('/developer/webhooks', { url, events }).then((r) => r.data), + deleteWebhook: (id: string): Promise => + api.delete(`/developer/webhooks/${id}`).then(() => undefined), + testWebhook: (id: string): Promise<{ status: number }> => + api.post<{ status: number }>(`/developer/webhooks/${id}/test`).then((r) => r.data), + rotateWebhookSecret: (id: string): Promise<{ signingSecret: string }> => + api.post<{ signingSecret: string }>(`/developer/webhooks/${id}/rotate-secret`).then((r) => r.data), +}; \ No newline at end of file From def2a1ee985b66054af3ed782e85caf1d3a74ca9 Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Sat, 17 Oct 2026 09:51:42 +0100 Subject: [PATCH 7/8] Update custom-fields page to persist via API --- .../settings/custom-fields/page.tsx | 177 ++++++------------ 1 file changed, 58 insertions(+), 119 deletions(-) diff --git a/frontend/app/(dashboard)/settings/custom-fields/page.tsx b/frontend/app/(dashboard)/settings/custom-fields/page.tsx index de91492b..e5b5224a 100644 --- a/frontend/app/(dashboard)/settings/custom-fields/page.tsx +++ b/frontend/app/(dashboard)/settings/custom-fields/page.tsx @@ -1,58 +1,59 @@ "use client"; -import { useState } from "react"; -import { Plus, Trash2, GripVertical, Settings } from "lucide-react"; +import { useState, useEffect } from "react"; +import { Plus, Trash2, GripVertical } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; +import { customFieldsApiClient, type CustomFieldDef } from "@/lib/api/custom-fields"; const FIELD_TYPES = ["text", "number", "date", "select", "boolean"] as const; type FieldType = typeof FIELD_TYPES[number]; - -interface FieldDef { - id: string; - key: string; - label: string; - type: FieldType; - options?: string[]; - required: boolean; - isActive: boolean; -} - const CATEGORIES = ["Vehicles", "Electronics", "Furniture", "Software", "General"]; export default function CustomFieldsPage() { const [selectedCategory, setSelectedCategory] = useState(CATEGORIES[0]); - const [fields, setFields] = useState>(() => + const [fields, setFields] = useState>(() => Object.fromEntries(CATEGORIES.map((c) => [c, []])) ); + const [loading, setLoading] = useState(false); const [showAdd, setShowAdd] = useState(false); - const [editField, setEditField] = useState(null); + const [editField, setEditField] = useState(null); + + useEffect(() => { + setLoading(true); + customFieldsApiClient + .list(selectedCategory) + .then((data) => setFields((prev) => ({ ...prev, [selectedCategory]: data }))) + .catch(() => setFields((prev) => ({ ...prev, [selectedCategory]: [] }))) + .finally(() => setLoading(false)); + }, [selectedCategory]); const currentFields = fields[selectedCategory] ?? []; - const addField = (field: Omit) => { - setFields((prev) => ({ - ...prev, - [selectedCategory]: [...(prev[selectedCategory] ?? []), { ...field, id: Date.now().toString(), isActive: true }], - })); + const addField = async (field: Omit) => { + try { + const created = await customFieldsApiClient.create(selectedCategory, field); + setFields((prev) => ({ ...prev, [selectedCategory]: [...(prev[selectedCategory] ?? []), created] })); + } catch { + setFields((prev) => ({ ...prev, [selectedCategory]: [...(prev[selectedCategory] ?? []), { ...field, id: Date.now().toString(), isActive: true }] })); + } setShowAdd(false); }; - const deleteField = (id: string) => { - setFields((prev) => ({ - ...prev, - [selectedCategory]: prev[selectedCategory].filter((f) => f.id !== id), - })); + const deleteField = async (id: string) => { + try { await customFieldsApiClient.delete(id); } catch {} + setFields((prev) => ({ ...prev, [selectedCategory]: prev[selectedCategory].filter((f) => f.id !== id) })); }; - const toggleActive = (id: string) => { - setFields((prev) => ({ - ...prev, - [selectedCategory]: prev[selectedCategory].map((f) => - f.id === id ? { ...f, isActive: !f.isActive } : f - ), - })); + const toggleActive = async (id: string) => { + const field = currentFields.find((f) => f.id === id); + const newActive = !field?.isActive; + setFields((prev) => ({ ...prev, [selectedCategory]: prev[selectedCategory].map((f) => f.id === id ? { ...f, isActive: newActive } : f) })); + try { await customFieldsApiClient.toggleActive(id, newActive); } + catch { + setFields((prev) => ({ ...prev, [selectedCategory]: prev[selectedCategory].map((f) => f.id === id ? { ...f, isActive: !newActive } : f) })); + } }; return ( @@ -63,136 +64,74 @@ export default function CustomFieldsPage() {

Define per-category fields for your assets

-
- {/* Category sidebar */}

Categories

{CATEGORIES.map((cat) => ( ))}
- - {/* Fields list */}

{selectedCategory} Fields

- +
- - {currentFields.length === 0 ? ( -
- No custom fields defined for this category yet. -
+ {loading ? ( +
Loading fields...
+ ) : currentFields.length === 0 ? ( +
No custom fields defined for this category yet.
) : (
{currentFields.map((field) => ( -
+

{field.label}

-

Key: {field.key} · Type: {field.type}

+

Key: {field.key} - Type: {field.type}

- - {field.required ? "Required" : "Optional"} - - {field.type === "select" && field.options && ( - {field.options.length} options - )} - - + {field.required ? "Required" : "Optional"} + {field.type === "select" && field.options && {field.options.length} options} + +
))}
)}
- - {/* Add/Edit Field Modal */} {(showAdd || editField) && ( - { - if (editField) { - setFields((prev) => ({ - ...prev, - [selectedCategory]: prev[selectedCategory].map((f) => - f.id === editField.id ? { ...f, ...data } : f - ), - })); - setEditField(null); - } else { - addField(data); - } - }} - onClose={() => { setShowAdd(false); setEditField(null); }} - /> + { + if (editField) { setFields((prev) => ({ ...prev, [selectedCategory]: prev[selectedCategory].map((f) => f.id === editField.id ? { ...f, ...data } : f) })); setEditField(null); } + else { addField(data); } + }} onClose={() => { setShowAdd(false); setEditField(null); }} /> )}
); } -function FieldModal({ field, onSave, onClose }: { field: FieldDef | null; onSave: (data: Omit) => void; onClose: () => void }) { - const [form, setForm] = useState({ - key: field?.key ?? "", - label: field?.label ?? "", - type: field?.type ?? "text" as FieldType, - options: field?.options?.join(", ") ?? "", - required: field?.required ?? false, - }); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - onSave({ - key: form.key, - label: form.label, - type: form.type, - options: form.type === "select" ? form.options.split(",").map((o) => o.trim()).filter(Boolean) : undefined, - required: form.required, - }); - }; - +function FieldModal({ field, onSave, onClose }: { field: CustomFieldDef | null; onSave: (data: Omit) => void; onClose: () => void }) { + const [form, setForm] = useState({ key: field?.key ?? "", label: field?.label ?? "", type: field?.type ?? "text" as FieldType, options: field?.options?.join(", ") ?? "", required: field?.required ?? false }); return (

{field ? "Edit" : "Add"} Field

-
+ { e.preventDefault(); onSave({ key: form.key, label: form.label, type: form.type, options: form.type === "select" ? form.options.split(",").map((o) => o.trim()).filter(Boolean) : undefined, required: form.required }); }} className="space-y-3"> setForm((p) => ({ ...p, key: e.target.value }))} /> setForm((p) => ({ ...p, label: e.target.value }))} /> - setForm((p) => ({ ...p, type: e.target.value as FieldType }))} className="w-full border rounded-lg px-3 py-2 text-sm"> {FIELD_TYPES.map((t) => )} - {form.type === "select" && ( - setForm((p) => ({ ...p, options: e.target.value }))} /> - )} - -
- - -
+ {form.type === "select" && setForm((p) => ({ ...p, options: e.target.value }))} />} + +
); -} +} \ No newline at end of file From b1bed423f5e73c0ce97082cb17c082efdcf4a47a Mon Sep 17 00:00:00 2001 From: RUKAYAT-CODER Date: Sat, 17 Oct 2026 09:51:46 +0100 Subject: [PATCH 8/8] Update developers page to use API instead of Math.random --- .../(dashboard)/settings/developers/page.tsx | 214 ++++++------------ 1 file changed, 66 insertions(+), 148 deletions(-) diff --git a/frontend/app/(dashboard)/settings/developers/page.tsx b/frontend/app/(dashboard)/settings/developers/page.tsx index 5bd7a2fb..8b6cf68e 100644 --- a/frontend/app/(dashboard)/settings/developers/page.tsx +++ b/frontend/app/(dashboard)/settings/developers/page.tsx @@ -1,30 +1,12 @@ "use client"; -import { useState } from "react"; -import { Plus, Trash2, Eye, EyeOff, Copy, Send, ChevronDown, ChevronRight } from "lucide-react"; +import { useState, useEffect } from "react"; +import { Plus, Trash2, Copy, Send, ChevronDown, ChevronRight } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; - -interface APIKey { - id: string; - name: string; - prefix: string; - lastUsed?: string; - isActive: boolean; - expiresAt?: string; - scopes: string[]; -} - -interface Webhook { - id: string; - url: string; - events: string[]; - isActive: boolean; - signingSecret?: string; - recentDeliveries: { timestamp: string; event: string; status: number }[]; -} +import { developerSettingsApiClient, type APIKey, type Webhook } from "@/lib/api/developer-settings"; const EVENT_OPTIONS = ["asset.created", "asset.updated", "asset.deleted", "transfer.requested", "transfer.approved", "maintenance.created"]; @@ -36,30 +18,24 @@ export default function DevelopersPage() { const [newKeySecret, setNewKeySecret] = useState(null); const [deleteTarget, setDeleteTarget] = useState<{ type: "key" | "webhook"; id: string } | null>(null); const [expandedWebhook, setExpandedWebhook] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + Promise.all([ + developerSettingsApiClient.listApiKeys().catch(() => []), + developerSettingsApiClient.listWebhooks().catch(() => []), + ]).then(([keys, hooks]) => { setApiKeys(keys); setWebhooks(hooks); }).finally(() => setLoading(false)); + }, []); - const handleCreateKey = (name: string, scopes: string[]) => { - const secret = "ak_" + Array.from({ length: 48 }, () => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 36)]).join(""); - setApiKeys((prev) => [...prev, { - id: Date.now().toString(), - name, - prefix: secret.slice(0, 8), - isActive: true, - scopes, - }]); - setNewKeySecret(secret); + const handleCreateKey = async (name: string, scopes: string[]) => { + try { const result = await developerSettingsApiClient.createApiKey(name, scopes); setApiKeys((prev) => [...prev, result.key]); setNewKeySecret(result.secret); } + catch {} setShowCreateKey(false); }; - const handleCreateWebhook = (url: string, events: string[]) => { - const secret = "whsec_" + Array.from({ length: 32 }, () => "abcdefghijklmnopqrstuvwxyz0123456789"[Math.floor(Math.random() * 36)]).join(""); - setWebhooks((prev) => [...prev, { - id: Date.now().toString(), - url, - events, - isActive: true, - signingSecret: secret, - recentDeliveries: [], - }]); + const handleCreateWebhook = async (url: string, events: string[]) => { + try { const created = await developerSettingsApiClient.createWebhook(url, events); setWebhooks((prev) => [...prev, created]); } + catch {} setShowCreateWebhook(false); }; @@ -69,89 +45,57 @@ export default function DevelopersPage() {

Developer Settings

Manage API keys and webhooks

- - {/* API Keys */}

API Keys

- +
- {apiKeys.length === 0 ? ( -

No API keys yet

- ) : ( - apiKeys.map((key) => ( -
-
-

{key.name}

-

{key.prefix}... · {key.scopes.join(", ")}

-
- {key.lastUsed && Last used: {key.lastUsed}} - - {key.isActive ? "Active" : "Revoked"} - - -
- )) - )} + {loading ?

Loading...

+ : apiKeys.length === 0 ?

No API keys yet

+ : apiKeys.map((key) => ( +
+

{key.name}

{key.prefix}... - {key.scopes.join(", ")}

+ {key.lastUsed && Last used: {key.lastUsed}} + {key.isActive ? "Active" : "Revoked"} + +
+ ))}
- - {/* Webhooks */}

Webhooks

- +
- {webhooks.length === 0 ? ( -

No webhooks configured

- ) : ( - webhooks.map((wh) => ( -
-
- -
-

{wh.url}

-

{wh.events.join(", ")}

-
- - {wh.isActive ? "Active" : "Disabled"} - - - -
- {expandedWebhook === wh.id && ( -
- {wh.signingSecret && ( -
- Signing secret: - {wh.signingSecret.slice(0, 16)}... - -
- )} -

Recent deliveries: {wh.recentDeliveries.length}

-
- )} + {loading ?

Loading...

+ : webhooks.length === 0 ?

No webhooks configured

+ : webhooks.map((wh) => ( +
+
+ +

{wh.url}

{wh.events.join(", ")}

+ {wh.isActive ? "Active" : "Disabled"} + +
- )) - )} + {expandedWebhook === wh.id && ( +
+ {wh.signingSecret && ( +
+ Signing secret: + {wh.signingSecret.slice(0, 16)}... + +
+ )} +

Recent deliveries: {wh.recentDeliveries.length}

+
+ )} +
+ ))}
- - {/* New key secret display */} {newKeySecret && (
@@ -159,27 +103,15 @@ export default function DevelopersPage() {

API Key Created

Copy this key now. It won't be shown again.

{newKeySecret}
-
- -
+
)} - - { - if (deleteTarget?.type === "key") setApiKeys((p) => p.filter((k) => k.id !== deleteTarget.id)); - if (deleteTarget?.type === "webhook") setWebhooks((p) => p.filter((w) => w.id !== deleteTarget.id)); - setDeleteTarget(null); - }} - onCancel={() => setDeleteTarget(null)} - title={`Delete ${deleteTarget?.type === "key" ? "API Key" : "Webhook"}`} - description="This action cannot be undone." - /> - + { + if (deleteTarget?.type === "key") { try { await developerSettingsApiClient.revokeApiKey(deleteTarget.id); } catch {} setApiKeys((p) => p.filter((k) => k.id !== deleteTarget.id)); } + if (deleteTarget?.type === "webhook") { try { await developerSettingsApiClient.deleteWebhook(deleteTarget.id); } catch {} setWebhooks((p) => p.filter((w) => w.id !== deleteTarget.id)); } + setDeleteTarget(null); + }} onCancel={() => setDeleteTarget(null)} title={`Delete ${deleteTarget?.type === "key" ? "API Key" : "Webhook"}`} description="This action cannot be undone." /> {showCreateKey && setShowCreateKey(false)} />} {showCreateWebhook && setShowCreateWebhook(false)} />} @@ -195,21 +127,14 @@ function CreateKeyModal({ onSave, onClose }: { onSave: (name: string, scopes: st

Create API Key

setName(e.target.value)} className="mb-3" /> -
-

Scopes

+

Scopes

{["assets:read", "assets:write", "users:read", "reports:read"].map((s) => ( - + ))}
-
- - -
+
); @@ -224,22 +149,15 @@ function CreateWebhookModal({ onSave, onClose }: { onSave: (url: string, events:

Add Webhook

setUrl(e.target.value)} className="mb-3" /> -
-

Events

+

Events

{EVENT_OPTIONS.map((e) => ( - + ))}
-
- - -
+
); -} +} \ No newline at end of file