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 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 18:18:36 +08:00
parent 41b52750d8
commit c8152ce86e
25 changed files with 2068 additions and 86 deletions

View File

@@ -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<Orgs>) => ({

View File

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

View File

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

View File

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