import { NextResponse } from "next/server"; import { z } from "zod"; import { AdminAuthorizationError, requirePermission, type AdminPermission, type AdminSession, } from "./auth"; import { ADMIN_MFA_PROOF_COOKIE, HIGH_RISK_ADMIN_PROOF_COOKIE, isSameOriginAdminMutation, resolveAdminMfaStatus, verifyAdminMfaProof, verifyHighRiskAdminProof, type AdminMfaStatus, } from "./auth-policy"; import { isPostgresError } from "./database"; export const listQuerySchema = z.object({ page: z.coerce.number().int().min(1).default(1), pageSize: z.coerce.number().int().min(1).max(100).default(20), sort: z.string().trim().max(64).optional(), order: z.enum(["asc", "desc"]).default("desc"), q: z.string().trim().max(200).optional(), status: z.string().trim().max(50).optional(), }); export type ListQuery = z.infer; export function parseListQuery(request: Request) { return listQuerySchema.safeParse( Object.fromEntries(new URL(request.url).searchParams.entries()), ); } export function requestId(request: Request): string { const supplied = request.headers.get("x-request-id")?.trim(); return supplied && supplied.length <= 200 ? supplied : crypto.randomUUID(); } export function adminErrorResponse(error: unknown) { if (error instanceof AdminAuthorizationError) { return NextResponse.json({ error: error.message }, { status: error.status }); } if (isPostgresError(error)) { if (error.code === "42501") return NextResponse.json({ error: "无权执行此操作" }, { status: 403 }); if (error.code === "40001") return NextResponse.json({ error: "资源已被其他管理员修改,请刷新后重试" }, { status: 409 }); if (error.code === "22023" || error.code === "23514" || error.code === "23505") { return NextResponse.json({ error: "提交内容不符合业务约束" }, { status: 400 }); } } return NextResponse.json( { error: "后台服务暂时不可用" }, { status: 500 }, ); } export async function requireAdminMutation(request: Request, permission: AdminPermission) { if (!isSameOriginAdminMutation(request.headers.get("origin"), request.url)) { throw new AdminAuthorizationError("请求来源不可信", 403); } return requirePermission(permission, request.headers); } export function requestCookie(request: Request, name: string): string | undefined { for (const part of request.headers.get("cookie")?.split(";") ?? []) { const separator = part.indexOf("="); if (separator > 0 && part.slice(0, separator).trim() === name) { return part.slice(separator + 1).trim(); } } } export function adminProofSigningSecret( env: NodeJS.ProcessEnv = process.env, ): string { const secret = env.BETTER_AUTH_USER_SECRET?.trim(); if (!secret || secret.length < 32) { throw new AdminAuthorizationError("MFA 服务暂时不可用", 503); } return secret; } export function readAdminMfaStatus( request: Request, session: AdminSession, ): AdminMfaStatus { const enrolled = session.user.twoFactorEnabled === true; const verified = enrolled && verifyAdminMfaProof( requestCookie(request, ADMIN_MFA_PROOF_COOKIE), { userId: session.user.id, sessionId: session.identitySession.id, origin: new URL(request.url).origin, }, adminProofSigningSecret(), session.identitySession.token, ); return resolveAdminMfaStatus(session.requiresMfa, enrolled, verified); } export function requireAdminMfaIfRequired( request: Request, session: AdminSession, ): AdminMfaStatus { const status = readAdminMfaStatus(request, session); if (!status.required) return status; if (!status.enrolled) { throw new AdminAuthorizationError("此管理员角色必须先启用 MFA", 403); } if (!status.verified) { throw new AdminAuthorizationError("请先完成当前会话的 MFA 验证", 403); } return status; } export async function requireHighRiskAdminMutation( request: Request, permission: AdminPermission, ): Promise { const session = await requireAdminMutation(request, permission); requireAdminMfaIfRequired(request, session); const origin = new URL(request.url).origin; const valid = verifyHighRiskAdminProof( requestCookie(request, HIGH_RISK_ADMIN_PROOF_COOKIE), { userId: session.user.id, sessionId: session.identitySession.id, permission, origin, }, adminProofSigningSecret(), session.identitySession.token, ); if (!valid) { throw new AdminAuthorizationError("请先使用邮箱验证码重新认证", 403); } return session; } export function invalidQueryResponse(details?: unknown) { return NextResponse.json( { error: "查询参数不正确", ...(details ? { details } : {}) }, { status: 400 }, ); } export async function readonlyAdminMutation() { try { const { requireAdminSession } = await import("./auth"); await requireAdminSession(); return NextResponse.json({ error: "此资源只读" }, { status: 405 }); } catch (error) { return adminErrorResponse(error); } }