diff --git a/server/main.ts b/server/main.ts index 82f5607..e675f75 100644 --- a/server/main.ts +++ b/server/main.ts @@ -1097,6 +1097,32 @@ app.post("/api/admin/users/:id/status", async (c) => { return c.json({ success: true }); }); +app.post("/api/admin/users/:id/profile", async (c) => { + const auth = await getAuthenticatedUser(c); + if (!auth) return c.json({ error: "Unauthorized" }, 401); + + if (!(await isGlobalAdmin(auth.userId))) { + return c.json({ error: "Forbidden: Global admin access required" }, 403); + } + + const targetUserId = c.req.param("id"); + const { displayName } = await c.req.json(); + + const targetUser = await sqlWrapper + .sql`UPDATE users SET display_name = ${ + displayName?.trim() || null + } WHERE id = ${targetUserId} RETURNING id, username, display_name` + .then((res: any) => res[0]); + + if (!targetUser) return c.json({ error: "User not found" }, 404); + + auditWrapper.auditLog(auth.userId, "user_profile_updated", targetUserId, { + display_name: targetUser.display_name, + }, getClientIp(c)); + + return c.json({ success: true, user: targetUser }); +}); + // --------------------------------------------------------- // Traefik ForwardAuth Edge Proxy Route (Tier 2) // --------------------------------------------------------- @@ -1309,6 +1335,52 @@ app.post("/api/admin/apps", async (c) => { } }); +app.put("/api/admin/apps/:id", async (c) => { + const auth = await getAuthenticatedUser(c); + if (!auth) return c.json({ error: "Unauthorized" }, 401); + if (!(await isGlobalAdmin(auth.userId))) { + return c.json({ error: "Forbidden" }, 403); + } + + const appId = c.req.param("id"); + const { + name, + description, + domain, + is_public, + bypass_paths, + allowed_cidrs, + } = await c.req.json(); + + if (!name) { + return c.json({ error: "Application name is required" }, 400); + } + + try { + const updatedApp = await sqlWrapper.sql` + UPDATE apps + SET name = ${name.trim()}, + description = ${description?.trim() || null}, + domain = ${domain?.trim() || null}, + is_public = ${is_public || false}, + bypass_paths = ${bypass_paths || []}, + allowed_cidrs = ${allowed_cidrs || []} + WHERE id = ${appId} + RETURNING id, name, spiffe_id, description, domain, is_public, bypass_paths, allowed_cidrs + `.then((res: any) => res[0]); + + if (!updatedApp) return c.json({ error: "Application not found" }, 404); + + auditWrapper.auditLog(auth.userId, "app_updated", updatedApp.id, { + name: updatedApp.name, + }, getClientIp(c)); + + return c.json({ success: true, app: updatedApp }); + } catch (_err: any) { + return c.json({ error: "Failed to update application" }, 500); + } +}); + app.delete("/api/admin/apps/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); @@ -1429,6 +1501,43 @@ app.post("/api/admin/roles", async (c) => { } }); +app.put("/api/admin/roles/:id", async (c) => { + const auth = await getAuthenticatedUser(c); + if (!auth) return c.json({ error: "Unauthorized" }, 401); + if (!(await isGlobalAdmin(auth.userId))) { + return c.json({ error: "Forbidden" }, 403); + } + + const roleId = c.req.param("id"); + const { name, description } = await c.req.json(); + + if (!name || typeof name !== "string" || name.trim().length < 2) { + return c.json({ error: "Role name must be at least 2 characters" }, 400); + } + + const normalizedName = name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "_"); + + try { + const updatedRole = await sqlWrapper.sql` + UPDATE roles + SET name = ${normalizedName}, + description = ${description?.trim() || null} + WHERE id = ${roleId} + RETURNING id, name, description, app_id, created_at + `.then((res: any) => res[0]); + + if (!updatedRole) return c.json({ error: "Role not found" }, 404); + + auditWrapper.auditLog(auth.userId, "role_updated", updatedRole.app_id, { + role_name: updatedRole.name, + }, getClientIp(c)); + + return c.json({ success: true, role: updatedRole }); + } catch (_err: any) { + return c.json({ error: "Failed to update role" }, 500); + } +}); + app.delete("/api/admin/roles/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); diff --git a/ui/components/AdminAppsPage.tsx b/ui/components/AdminAppsPage.tsx index 7ab2452..e0da117 100644 --- a/ui/components/AdminAppsPage.tsx +++ b/ui/components/AdminAppsPage.tsx @@ -9,39 +9,78 @@ export const AdminAppsPage = ({