import { SESSION_COOKIE_NAME, getAuthRuntimeConfig, safeNextPath, shouldUseSecureAuthCookie } from "@/lib/auth/config"; import { createSessionCookieValue } from "@/lib/auth/session"; import { jsonError, jsonOk, readJsonBody } from "@/lib/server/api"; import { createSessionFromClaims, verifyAuthJwt } from "@/lib/server/auth/jwt"; export const runtime = "nodejs"; type PasswordTokenResponse = { access_token?: string; refresh_token?: string; expires_in?: string | number; token_type?: string; error?: string; error_description?: string; msg?: string; message?: string; [key: string]: unknown; }; export async function POST(request: Request) { try { const config = getAuthRuntimeConfig(); if (!config.configured || !config.tokenUrl || !config.clientSecret || !config.sessionSecret) { throw new PasswordLoginError(`认证配置不完整:${config.missing.join(", ") || "未知配置"}`, 500); } const body = await readJsonBody<{ username?: string; password?: string; code?: string; randomStr?: string; next?: string; }>(request); const username = body.username?.trim(); const password = body.password || ""; const code = body.code?.trim(); const randomStr = body.randomStr?.trim(); if (!username || !password || !code || !randomStr) throw new PasswordLoginError("账号、密码和验证码不能为空。"); const token = await exchangePasswordToken({ tokenUrl: config.tokenUrl, clientId: config.clientId, clientSecret: config.clientSecret, scope: config.scope, username, password, code, randomStr }); if (!token.access_token) throw new PasswordLoginError("认证中心没有返回 access_token。", 502); const claims = await verifyAuthJwt(token.access_token, config); const session = createSessionFromClaims(claims, config, parseExpiresIn(token.expires_in)); const response = jsonOk({ ok: true, redirectTo: safeNextPath(body.next), user: session.user }); response.cookies.set(SESSION_COOKIE_NAME, await createSessionCookieValue(session, config.sessionSecret), { httpOnly: true, sameSite: "lax", secure: shouldUseSecureAuthCookie(request.url), path: "/", expires: new Date(session.expiresAt * 1000) }); return response; } catch (error) { return jsonError(error, 401); } } async function exchangePasswordToken(input: { tokenUrl: string; clientId: string; clientSecret: string; scope: string; username: string; password: string; code: string; randomStr: string; }): Promise { const form = new URLSearchParams(); form.set("grant_type", "password"); form.set("scope", input.scope); form.set("username", input.username); form.set("password", input.password); form.set("code", input.code); form.set("randomStr", input.randomStr); const response = await fetch(input.tokenUrl, { method: "POST", headers: { Authorization: `Basic ${Buffer.from(`${input.clientId}:${input.clientSecret}`).toString("base64")}`, "Content-Type": "application/x-www-form-urlencoded" }, body: form }); const payload = await response.json().catch(() => ({})) as PasswordTokenResponse; if (!response.ok) { throw new PasswordLoginError(payload.error_description || payload.msg || payload.message || payload.error || "登录失败。", response.status); } return payload; } function parseExpiresIn(value: string | number | undefined): number | undefined { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim()) { const parsed = Number(value); if (Number.isFinite(parsed)) return parsed; } return undefined; } class PasswordLoginError extends Error { status: number; constructor(message: string, status = 400) { super(message); this.name = "PasswordLoginError"; this.status = status; } }