feat(01-05): super-admin API routes and middleware guard

- withSuperAdmin() HOF: checks isSuperAdmin from session, returns 401/403
- GET /api/admin/tenants: lists all tenants with userCount, subscriberCount
- GET /api/admin/tenants/[id]: single tenant detail with users list
- POST /api/admin/tenants/[id]/suspend: suspend/activate with 7-day grace
- prisma/seed.ts: add Test ISP 2 tenant and admin2@demo.com for isolation tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kevin-asprec
2026-03-04 19:04:10 +08:00
parent d25885aaeb
commit df40eae328
5 changed files with 342 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
/**
* The context passed to super-admin route handlers.
* Handlers receive the session user confirmed to be a super-admin.
*/
export interface SuperAdminContext {
user: {
id: string;
email: string;
tenantId: string | null;
roles: string[];
isSuperAdmin: boolean;
firstName: string;
lastName: string;
};
}
/**
* Handler signature for super-admin route handlers.
* Receives the request and super-admin context with user info.
* Optionally receives route segment params (for dynamic routes).
*/
type SuperAdminHandler<P = Record<string, string>> = (
req: NextRequest,
ctx: SuperAdminContext,
params?: P
) => Promise<NextResponse> | NextResponse;
/**
* Higher-order function that wraps a Next.js route handler with super-admin enforcement.
*
* Flow:
* 1. Get current user session via getCurrentUser()
* 2. If no session → return 401 Unauthorized
* 3. If user.isSuperAdmin is not true → return 403 Forbidden
* 4. If authorized → call handler with super-admin context
*
* Usage (static route):
* ```typescript
* export const GET = withSuperAdmin(async (req, { user }) => {
* const tenants = await prisma.tenant.findMany();
* return NextResponse.json(tenants);
* });
* ```
*
* Usage (dynamic route with params):
* ```typescript
* export const GET = withSuperAdmin(async (req, { user }, params) => {
* const { id } = params;
* return NextResponse.json({ id });
* });
* ```
*
* @param handler - The route handler to wrap with super-admin protection
* @returns A Next.js route handler function
*/
export function withSuperAdmin<P = Record<string, string>>(
handler: SuperAdminHandler<P>
) {
return async function (
req: NextRequest,
routeContext?: { params: P | Promise<P> }
): Promise<NextResponse> {
// Step 1: Get session user
const user = await getCurrentUser();
// Step 2: No session → 401 Unauthorized
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Step 3: Not super-admin → 403 Forbidden
if (!user.isSuperAdmin) {
return NextResponse.json(
{ error: "Super-admin access required" },
{ status: 403 }
);
}
// Step 4: Resolve params (Next.js 15 may return Promise<params>)
let resolvedParams: P | undefined;
if (routeContext?.params) {
resolvedParams = routeContext.params instanceof Promise
? await routeContext.params
: routeContext.params;
}
// Step 5: Authorized → call handler with context and resolved params
return handler(req, { user }, resolvedParams);
};
}