import { compareSync } from 'bcryptjs' import { z } from 'zod' import { getAdminSession } from '../../utils/auth' import { useDatabase } from '../../utils/db' import { writeAuditLog } from '../../utils/audit' const loginSchema = z.object({ username: z.string().trim().min(1).max(80), password: z.string().min(8).max(200), }) export default defineEventHandler(async (event) => { const body = loginSchema.parse(await readBody(event)) const db = useDatabase() const user = db.prepare(` SELECT id, username, password_hash, display_name, status, failed_login_count, locked_until FROM admin_users WHERE username = ? `).get(body.username) as { id: number username: string password_hash: string display_name: string status: string failed_login_count: number locked_until: string | null } | undefined const locked = user?.locked_until && new Date(user.locked_until) > new Date() const valid = user && user.status === 'active' && !locked && compareSync(body.password, user.password_hash) if (!valid) { if (user && user.status === 'active') { const failures = user.failed_login_count + 1 const lockedUntil = failures >= 5 ? new Date(Date.now() + 10 * 60_000).toISOString() : null db.prepare('UPDATE admin_users SET failed_login_count = ?, locked_until = ?, updated_at = ? WHERE id = ?') .run(failures, lockedUntil, new Date().toISOString(), user.id) } writeAuditLog(event, { operatorId: user?.id, action: 'login_failed', entityType: 'admin_user', entityKey: body.username }) throw createError({ statusCode: 401, statusMessage: '账号、密码或验证码错误' }) } const timestamp = new Date().toISOString() db.prepare('UPDATE admin_users SET failed_login_count = 0, locked_until = NULL, last_login_at = ?, updated_at = ? WHERE id = ?') .run(timestamp, timestamp, user.id) const session = await getAdminSession(event) await session.update({ user: { id: user.id, username: user.username, displayName: user.display_name } }) writeAuditLog(event, { operatorId: user.id, action: 'login_success', entityType: 'admin_user', entityKey: String(user.id) }) return { user: session.data.user } })