// server/api/auth/login.post.ts import { consola } from "consola"; import type { IUserLoginData, IUserLoginRequest } from "#shared/types"; import { clearNewApiAuthCookies, createErrorResponse, createSuccessResponse, createUpstreamErrorResponse, extractCookieMetaFromSetCookie, extractCookieValueFromSetCookie, newApiFetchRaw, setNewApiAuthCookies } from "~~/server/utils"; interface INewApiWrappedLoginResponse { /** 登录成功时上游返回的用户信息 */ data?: INewApiLoginUserData | null; /** 上游提示信息 */ message?: string; /** 上游业务成功状态 */ success?: boolean; } type NewApiLoginResponse = INewApiLoginUserData | INewApiWrappedLoginResponse; interface INewApiLoginUserData extends IUserLoginData { /** NewAPI 登录结果如果返回额外字段,服务端会在返回前裁剪掉 */ [key: string]: unknown; } /** * 允许从请求体透传给上游的字段列表。 * 只有在这里声明过的字段才会被转发,其余字段一律丢弃,防止参数污染。 */ const LOGIN_FIELDS = ["username", "password"] as const satisfies ReadonlyArray< keyof IUserLoginRequest >; /** * POST /api/auth/login */ export default defineEventHandler(async (event) => { const requestBody = await readBody | null>(event); // 只接受 JSON 对象或 null,避免数组/字符串等无效 body 被透传到上游。 if (requestBody !== null && typeof requestBody !== "object") { clearNewApiAuthCookies(event); return createErrorResponse(400, "请求体必须是 JSON 对象"); } const payload: IUserLoginRequest = {}; for (const field of LOGIN_FIELDS) { const value = requestBody?.[field]; if (typeof value === "string") { payload[field] = value; } } try { // 登录必须读取上游 Set-Cookie,因此这里使用 raw fetch 保留响应头。 const result = await newApiFetchRaw( "/api/user/login", { method: "POST", body: payload, headers: { "Content-Type": "application/json" } } ); const normalized = normalizeLoginResponse(result._data); if (!normalized.user) { // 上游业务失败时同步清理本地旧登录态,避免继续携带坏 cookie。 clearNewApiAuthCookies(event); return createErrorResponse(1, normalized.message, result._data ?? null); } const session = extractCookieValueFromSetCookie(result.headers, "session"); if (!session) { // 没有 session 就不能恢复 NewAPI 登录态,必须让前端明确感知失败。 clearNewApiAuthCookies(event); consola.error( "/api/auth/login 登录成功但未收到 NewAPI session,请稍后重试" ); return createErrorResponse(500, "服务器错误,请稍后重试"); } setNewApiAuthCookies( event, session, normalized.user.id, extractCookieMetaFromSetCookie(result.headers, "session") ); return createSuccessResponse( normalized.user, normalized.message ); } catch (error) { clearNewApiAuthCookies(event); consola.error("/api/auth/login 登录失败", error); return createUpstreamErrorResponse(error, "登录失败"); } }); /** 兼容上游直接返回用户对象和 { success, message, data } 包装结构 */ const normalizeLoginResponse = ( response: NewApiLoginResponse | undefined ): { message: string; user: IUserLoginData | null } => { if (isUser(response)) { return { message: "登录成功", user: pickUserBasicData(response) }; } if (!isRecord(response)) { consola.error("/api/auth/login 登录失败:响应不是对象"); return { message: "登录失败", user: null }; } const wrapped = response as INewApiWrappedLoginResponse; const message = wrapped.message || (wrapped.success ? "登录成功" : "登录失败"); if (wrapped.success !== true || !isUser(wrapped.data)) { consola.error("/api/auth/login 登录失败:上游返回的数据格式不正确"); return { message, user: null }; } return { message, user: pickUserBasicData(wrapped.data) }; }; /** 只返回前端需要的基础用户字段,避免上游额外字段透出 */ const pickUserBasicData = (user: INewApiLoginUserData): IUserLoginData => { return { id: user.id, username: user.username, display_name: user.display_name, group: user.group, role: user.role, status: user.status }; }; const isUser = (value: unknown): value is INewApiLoginUserData => { return ( isRecord(value) && typeof value.id === "number" && typeof value.username === "string" && typeof value.display_name === "string" && typeof value.group === "string" && typeof value.role === "number" && typeof value.status === "number" ); }; const isRecord = (value: unknown): value is Record => { return value !== null && typeof value === "object"; };