import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto"; import { cookies } from "next/headers"; import type { Account, AuthContext, Permission, PublicAccount, RoleId, Session } from "@/modules/core/types"; import { jsonFailure } from "@/modules/core/server/api"; import { recordAuditLog } from "@/modules/core/server/audit"; import { getPermissionsForRole, hasPermission } from "@/modules/core/server/permissions"; import { readData, updateData } from "@/modules/core/server/store"; const SESSION_COOKIE = "teatea_session"; const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7; export function normalizeEmail(email: string): string { return email.trim().toLowerCase(); } export function toPublicAccount(account: Account): PublicAccount { return { id: account.id, name: account.name, email: account.email, role: account.role, organization: account.organization, status: account.status, createdAt: account.createdAt, updatedAt: account.updatedAt, }; } export function hashPassword(password: string, salt = randomBytes(16).toString("hex")): { hash: string; salt: string; } { return { hash: scryptSync(password, salt, 64).toString("hex"), salt, }; } export function verifyPassword(password: string, account: Account): boolean { const candidate = Buffer.from(hashPassword(password, account.passwordSalt).hash, "hex"); const stored = Buffer.from(account.passwordHash, "hex"); if (candidate.length !== stored.length) { return false; } return timingSafeEqual(candidate, stored); } export function createAccountRecord(input: { name: string; email: string; password: string; role: RoleId; organization?: string; }): Account { const now = new Date().toISOString(); const password = hashPassword(input.password); return { id: randomUUID(), name: input.name.trim(), email: normalizeEmail(input.email), role: input.role, organization: input.organization?.trim(), passwordHash: password.hash, passwordSalt: password.salt, status: "active", createdAt: now, updatedAt: now, }; } export function createSessionRecord(accountId: string): Session { const now = new Date(); const expiresAt = new Date(now.getTime() + SESSION_MAX_AGE_SECONDS * 1000); return { id: randomUUID(), accountId, createdAt: now.toISOString(), expiresAt: expiresAt.toISOString(), }; } export async function setSessionCookie(sessionId: string): Promise { const cookieStore = await cookies(); cookieStore.set({ name: SESSION_COOKIE, value: sessionId, httpOnly: true, sameSite: "lax", path: "/", maxAge: SESSION_MAX_AGE_SECONDS, }); } export async function clearSessionCookie(): Promise { const cookieStore = await cookies(); cookieStore.delete(SESSION_COOKIE); } export async function getBootstrapState(): Promise<{ setupRequired: boolean }> { const data = await readData(); return { setupRequired: data.accounts.length === 0 }; } export async function getCurrentAuthContext(): Promise { const cookieStore = await cookies(); const sessionId = cookieStore.get(SESSION_COOKIE)?.value; if (!sessionId) { return null; } const data = await readData(); const session = data.sessions.find((item) => item.id === sessionId); if (!session || Date.parse(session.expiresAt) <= Date.now()) { return null; } const account = data.accounts.find((item) => item.id === session.accountId); if (!account || account.status !== "active") { return null; } return { account: toPublicAccount(account), permissions: getPermissionsForRole(account.role), session, }; } export async function removeCurrentSession(): Promise { const context = await getCurrentAuthContext(); const cookieStore = await cookies(); const sessionId = cookieStore.get(SESSION_COOKIE)?.value; if (sessionId) { await updateData((data) => { data.sessions = data.sessions.filter((item) => item.id !== sessionId); }); } await clearSessionCookie(); return context?.account ?? null; } type PermissionCheckSuccess = { success: true; context: AuthContext; }; type PermissionCheckFailure = { success: false; response: Response; }; type DeniedAuditContext = { action: string; targetType: string; targetId?: string; }; export async function requirePermission( permission: Permission, auditContext: DeniedAuditContext, ): Promise { const context = await getCurrentAuthContext(); if (!context) { await recordAuditLog({ action: auditContext.action, targetType: auditContext.targetType, targetId: auditContext.targetId, result: "denied", reason: "未登录或会话已过期", }); return { success: false, response: jsonFailure("未登录或会话已过期", 401), }; } if (!hasPermission(context.account.role, permission)) { await recordAuditLog({ actor: context.account, action: auditContext.action, targetType: auditContext.targetType, targetId: auditContext.targetId, result: "denied", reason: `缺少权限:${permission}`, }); return { success: false, response: jsonFailure("权限不足", 403), }; } return { success: true, context }; }