import { loginRequestSchema } from "@oj2/contract" import { eq, sql } from "drizzle-orm" import { Hono } from "hono" import { optionalAuth, type AppEnv } from "../auth/middleware" import { createSession, destroySession } from "../auth/session" import { publishSessionRevoked } from "../events" import { hashPassword, verifyPassword } from "../auth/password" import { db, schema } from "../db" import { failure, success } from "../http" import { getUserProfileById } from "../services/profile" export const authRoutes = new Hono() authRoutes.post("/auth/login", async (c) => { const parsed = loginRequestSchema.safeParse( await c.req.json().catch(() => null), ) if (!parsed.success) { return failure( c, 400, "invalid-request", "Username and password are required", ) } const [user] = await db .select() .from(schema.user) .where(sql`lower(${schema.user.username}) = lower(${parsed.data.username})`) .limit(1) if (!user) { return failure( c, 401, "invalid-credentials", "Invalid username or password", ) } if (user.isDisabled) { return failure(c, 403, "account-disabled", "Your account has been disabled") } const password = await verifyPassword(parsed.data.password, user.password) if (!password.valid) { return failure( c, 401, "invalid-credentials", "Invalid username or password", ) } const now = new Date().toISOString() const update: { lastLogin: string; password?: string } = { lastLogin: now } // 存量 pbkdf2 顺手升级成 argon2。生产库 1710 个账号都是 Django 写的 pbkdf2, // 靠这里随登录逐个迁移;没登录过的照旧由 verifyPassword 的 pbkdf2 分支兜着。 if (password.needsUpgrade) { update.password = await hashPassword(parsed.data.password) } await db.update(schema.user).set(update).where(eq(schema.user.id, user.id)) await createSession(c, user.id, user.lastLogin) return success(c, { ok: true }) }) authRoutes.delete("/auth/session", async (c) => { const token = await destroySession(c) // 同一个浏览器的其他标签页还挂着 WebSocket,页面上仍显示着登录态。推一条让它们 // 立刻清掉,不用等最多 60 秒的会话巡检。 // 按 token 而不是按用户:这个人在别的设备上的登录是另一张会话,不该被牵连。 if (token) await publishSessionRevoked({ token }, "session-ended") return success(c, null) }) authRoutes.get("/me", optionalAuth, async (c) => { const authUser = c.get("user") if (!authUser) return success(c, null) const data = await getUserProfileById(authUser.id, true) if (!data) return failure(c, 404, "profile-not-found", "User profile does not exist") return success(c, data) })