From c8152ce86e7b4ec304784ae07e43df164af71721 Mon Sep 17 00:00:00 2001 From: RevIQ Date: Fri, 9 Jan 2026 18:18:36 +0800 Subject: [PATCH 1/8] Implement Workstream M: Admin Pages (Frontend) Add superuser admin interface for managing organizations and users: - Admin layout with access control (redirects non-superusers) - Admin dashboard with org/user counts and quick actions - Org management: list, create, view/edit details, manage sites - User management: list, view details, toggle superuser, confirm email - SuperuserBadge component for consistent superuser indication - Sidebar shows admin link (shield icon) for superusers only - Centralized date formatting utility at $lib/utils/format-date.ts - Test plan documentation at docs/test-plans/admin.md Co-Authored-By: Claude Opus 4.5 --- .../src/procedures/admin/helpers.ts | 2 +- .../src/procedures/admin/users/create.ts | 4 +- .../src/procedures/admin/users/update.ts | 5 +- apps/api-server/src/router.ts | 8 +- .../src/lib/components/admin/index.ts | 1 + .../components/admin/superuser-badge.svelte | 9 + .../lib/components/layout/app-sidebar.svelte | 54 ++ .../lib/components/org/confirm-dialog.svelte | 4 +- .../src/lib/components/org/index.ts | 2 +- .../src/lib/utils/format-date.ts | 31 ++ .../src/routes/admin/+layout.svelte | 51 ++ .../src/routes/admin/+page.svelte | 112 +++++ .../src/routes/admin/orgs/+page.svelte | 215 ++++++++ .../src/routes/admin/orgs/[slug]/+page.svelte | 474 ++++++++++++++++++ .../src/routes/admin/orgs/new/+page.svelte | 160 ++++++ .../src/routes/admin/users/+page.svelte | 113 +++++ .../routes/admin/users/[email]/+page.svelte | 302 +++++++++++ .../src/routes/dashboard/+page.svelte | 29 +- .../routes/dashboard/[slug]/+layout.svelte | 60 ++- .../src/routes/dashboard/[slug]/+page.svelte | 25 +- .../dashboard/[slug]/members/+page.svelte | 98 +++- .../dashboard/[slug]/settings/+page.svelte | 44 +- .../src/routes/invite/accept/+page.svelte | 16 +- docs/initial-app.md | 18 +- docs/test-plans/admin.md | 317 ++++++++++++ 25 files changed, 2068 insertions(+), 86 deletions(-) create mode 100644 apps/publisher-dashboard/src/lib/components/admin/index.ts create mode 100644 apps/publisher-dashboard/src/lib/components/admin/superuser-badge.svelte create mode 100644 apps/publisher-dashboard/src/lib/utils/format-date.ts create mode 100644 apps/publisher-dashboard/src/routes/admin/+layout.svelte create mode 100644 apps/publisher-dashboard/src/routes/admin/+page.svelte create mode 100644 apps/publisher-dashboard/src/routes/admin/orgs/+page.svelte create mode 100644 apps/publisher-dashboard/src/routes/admin/orgs/[slug]/+page.svelte create mode 100644 apps/publisher-dashboard/src/routes/admin/orgs/new/+page.svelte create mode 100644 apps/publisher-dashboard/src/routes/admin/users/+page.svelte create mode 100644 apps/publisher-dashboard/src/routes/admin/users/[email]/+page.svelte create mode 100644 docs/test-plans/admin.md diff --git a/apps/api-server/src/procedures/admin/helpers.ts b/apps/api-server/src/procedures/admin/helpers.ts index 9043a46..d4614b8 100644 --- a/apps/api-server/src/procedures/admin/helpers.ts +++ b/apps/api-server/src/procedures/admin/helpers.ts @@ -2,8 +2,8 @@ * Admin procedure helpers - shared transformation functions */ +import type { OrgSites, Orgs, Users } from "@reviq/db-schema"; import type { Selectable } from "kysely"; -import type { Orgs, OrgSites, Users } from "@reviq/db-schema"; /** Transform org record to API response format */ export const toOrgResponse = (org: Selectable) => ({ diff --git a/apps/api-server/src/procedures/admin/users/create.ts b/apps/api-server/src/procedures/admin/users/create.ts index e749584..1e431a4 100644 --- a/apps/api-server/src/procedures/admin/users/create.ts +++ b/apps/api-server/src/procedures/admin/users/create.ts @@ -44,7 +44,7 @@ export const adminUsersCreate = os.admin.users.create .insertInto("users") .values({ email: normalizedEmail, - display_name: name || null, + display_name: name ?? null, }) .returning(["id"]) .executeTakeFirstOrThrow(); @@ -55,7 +55,7 @@ export const adminUsersCreate = os.admin.users.create .values({ org_id: orgId, user_id: newUser.id, - role: orgRole || "member", + role: orgRole ?? "member", }) .execute(); } diff --git a/apps/api-server/src/procedures/admin/users/update.ts b/apps/api-server/src/procedures/admin/users/update.ts index 8d6ef17..ddfc353 100644 --- a/apps/api-server/src/procedures/admin/users/update.ts +++ b/apps/api-server/src/procedures/admin/users/update.ts @@ -27,10 +27,7 @@ export const adminUsersUpdate = os.admin.users.update } // Prevent superuser from demoting themselves - if ( - isSuperuser === false && - normalizedEmail === context.user.email.toLowerCase() - ) { + if (!isSuperuser && normalizedEmail === context.user.email.toLowerCase()) { throw new ORPCError("BAD_REQUEST", { message: "Cannot remove your own superuser status", }); diff --git a/apps/api-server/src/router.ts b/apps/api-server/src/router.ts index e76f87f..a90fbfb 100644 --- a/apps/api-server/src/router.ts +++ b/apps/api-server/src/router.ts @@ -1,4 +1,5 @@ import { ORPCError } from "@orpc/server"; +import { adminRoutes } from "./procedures/admin/_routes.js"; import { createLoginRequest as createLoginRequestHandler } from "./procedures/auth/create-login-request.js"; import { forgotPassword as forgotPasswordHandler } from "./procedures/auth/forgot-password.js"; import { loginIfRequestIsCompleted as loginIfRequestIsCompletedHandler } from "./procedures/auth/login-if-completed.js"; @@ -9,8 +10,11 @@ import { resendVerificationEmail as resendVerificationHandler } from "./procedur import { resetPassword as resetPasswordHandler } from "./procedures/auth/reset-password.js"; import { signup as signupHandler } from "./procedures/auth/signup.js"; import { verifyEmail as verifyEmailHandler } from "./procedures/auth/verify-email.js"; -import { authMiddleware, loginRequestMiddleware, os } from "./procedures/base.js"; -import { adminRoutes } from "./procedures/admin/_routes.js"; +import { + authMiddleware, + loginRequestMiddleware, + os, +} from "./procedures/base.js"; import { meDelete } from "./procedures/me/delete.js"; import { getDeviceInfo, diff --git a/apps/publisher-dashboard/src/lib/components/admin/index.ts b/apps/publisher-dashboard/src/lib/components/admin/index.ts new file mode 100644 index 0000000..45c9048 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/admin/index.ts @@ -0,0 +1 @@ +export { default as SuperuserBadge } from "./superuser-badge.svelte"; diff --git a/apps/publisher-dashboard/src/lib/components/admin/superuser-badge.svelte b/apps/publisher-dashboard/src/lib/components/admin/superuser-badge.svelte new file mode 100644 index 0000000..ba13cfe --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/admin/superuser-badge.svelte @@ -0,0 +1,9 @@ + + + + + Superuser + diff --git a/apps/publisher-dashboard/src/lib/components/layout/app-sidebar.svelte b/apps/publisher-dashboard/src/lib/components/layout/app-sidebar.svelte index 3c64773..349d6a1 100644 --- a/apps/publisher-dashboard/src/lib/components/layout/app-sidebar.svelte +++ b/apps/publisher-dashboard/src/lib/components/layout/app-sidebar.svelte @@ -1,5 +1,7 @@