import { COOKIE_NAME, SESSION_TTL_MS } from "@shared/const"; import { parse as parseCookieHeader } from "cookie"; import { randomBytes, scrypt as scryptCallback, timingSafeEqual } from "crypto"; import type { Request } from "express"; import { SignJWT, jwtVerify } from "jose"; import { promisify } from "util"; import type { User } from "../../drizzle/schema"; import * as db from "../db"; import { ENV } from "./env"; const scrypt = promisify(scryptCallback); const PASSWORD_MIN_LENGTH = 8; function sessionSecret() { if (!ENV.cookieSecret || ENV.cookieSecret.length < 32) { throw new Error("JWT_SECRET 必须配置且至少 32 个字符"); } return new TextEncoder().encode(ENV.cookieSecret); } export function validatePassword(password: string) { if (password.length < PASSWORD_MIN_LENGTH) { throw new Error(`密码至少需要 ${PASSWORD_MIN_LENGTH} 位`); } } export async function hashPassword(password: string) { validatePassword(password); const salt = randomBytes(16); const derived = await scrypt(password, salt, 64) as Buffer; return `scrypt$${salt.toString("base64url")}$${derived.toString("base64url")}`; } export async function verifyPassword(password: string, encoded: string | null) { if (!encoded) return false; const [scheme, saltValue, hashValue] = encoded.split("$"); if (scheme !== "scrypt" || !saltValue || !hashValue) return false; try { const expected = Buffer.from(hashValue, "base64url"); const actual = await scrypt(password, Buffer.from(saltValue, "base64url"), expected.length) as Buffer; return actual.length === expected.length && timingSafeEqual(actual, expected); } catch { return false; } } export async function createSessionToken(user: Pick) { return new SignJWT({ userId: user.id, sessionVersion: user.sessionVersion }) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) .setIssuedAt() .setExpirationTime(Math.floor((Date.now() + SESSION_TTL_MS) / 1000)) .sign(sessionSecret()); } function readSessionCookie(req: Request) { if (!req.headers.cookie) return undefined; return parseCookieHeader(req.headers.cookie)[COOKIE_NAME]; } export async function authenticateRequest(req: Request): Promise { const token = readSessionCookie(req); if (!token) return null; try { const { payload } = await jwtVerify(token, sessionSecret(), { algorithms: ["HS256"] }); const userId = Number(payload.userId); const sessionVersion = Number(payload.sessionVersion); if (!Number.isInteger(userId) || !Number.isInteger(sessionVersion)) return null; const user = await db.getUserById(userId); if (!user || user.isActive !== 1 || user.sessionVersion !== sessionVersion) return null; return user; } catch { return null; } } type Attempt = { failures: number[] }; const attempts = new Map(); const RATE_WINDOW_MS = 15 * 60 * 1000; const RATE_MAX_FAILURES = 5; function normalizedUsername(username: string) { return username.trim().toLowerCase(); } function attemptKeys(ip: string, username: string) { return [`ip:${ip}`, `username:${normalizedUsername(username)}`]; } function recentFailures(key: string, now = Date.now()) { const record = attempts.get(key); if (!record) return []; record.failures = record.failures.filter(time => now - time < RATE_WINDOW_MS); if (record.failures.length === 0) attempts.delete(key); return record.failures; } export function isLoginRateLimited(ip: string, username: string) { return attemptKeys(ip, username).some(key => recentFailures(key).length >= RATE_MAX_FAILURES); } export function recordLoginFailure(ip: string, username: string) { const now = Date.now(); for (const key of attemptKeys(ip, username)) { const failures = recentFailures(key, now); failures.push(now); attempts.set(key, { failures }); } } export function clearLoginFailures(ip: string, username: string) { for (const key of attemptKeys(ip, username)) attempts.delete(key); } export function resetLoginRateLimitForTests() { attempts.clear(); } export async function ensureBootstrapAdmin() { if (await db.countActiveSuperAdmins() > 0) return; const username = ENV.bootstrapAdminUsername.trim(); const password = ENV.bootstrapAdminPassword; if (!username || !password) { throw new Error("系统不存在有效超级管理员,请配置 BOOTSTRAP_ADMIN_USERNAME 和 BOOTSTRAP_ADMIN_PASSWORD"); } validatePassword(password); const existing = await db.getUserByUsername(username); const passwordHash = await hashPassword(password); if (existing) { await db.updateUser(existing.id, { passwordHash, role: "super_admin", isActive: 1, mustChangePassword: 1, sessionVersion: existing.sessionVersion + 1, }); } else { await db.createUser({ username, passwordHash, name: "系统管理员", role: "super_admin", isActive: 1, mustChangePassword: 1, sessionVersion: 0, }); } console.log(`[Auth] 已创建引导超级管理员账号:${username}(首次登录必须修改密码)`); } export function publicUser(user: User) { const { passwordHash: _passwordHash, ...safe } = user; return safe; }