/** * 鉴权相关:登录、刷新令牌、本地会话读写。 * * 会话默认存 `localStorage`,键名见下方常量(与 `http.ts` 里读取的 `accessToken` 一致)。 */ import { postJson } from "./http"; import { ApiPath } from "./paths"; /** localStorage:访问令牌 */ export const ACCESS_TOKEN_KEY = "accessToken"; /** localStorage:刷新令牌 */ export const REFRESH_TOKEN_KEY = "refreshToken"; /** localStorage:用户信息 JSON 字符串 */ export const USER_KEY = "user"; export type AuthUser = Record; /** `ClientAuthUserDto`(与 OpenAPI 一致) */ export type ClientAuthUserDto = { id: string; phone: string; nickname: string; avatarUrl: string; }; /** 发送短信验证码时 `scene` 取值(与后端约定一致) */ export const AUTH_SMS_SCENE_LOGIN = "login"; /** `ClientSendSmsResponseDto` */ export type SmsSendResponse = { requestId: string; phone: string; scene: string; provider: string; expireIn: number; testCode?: string; }; /** `ClientLoginResponseDto` */ export type SmsLoginResponse = { accessToken: string; refreshToken: string; user: ClientAuthUserDto; }; /** `POST .../auth/refresh` 成功响应 */ export type RefreshTokenResponse = { accessToken: string; refreshToken: string; }; /** 写入一对令牌,并清理历史 mock 键 `token` */ export function persistTokens(accessToken: string, refreshToken: string): void { window.localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); window.localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); window.localStorage.removeItem("token"); } /** 写入用户信息(可选) */ export function persistUser(user: AuthUser | undefined): void { if (!user) return; window.localStorage.setItem(USER_KEY, JSON.stringify(user)); window.dispatchEvent(new CustomEvent("chatone-user-changed")); } /** 清除本地会话(含兼容旧键) */ export function clearSession(): void { window.localStorage.removeItem(ACCESS_TOKEN_KEY); window.localStorage.removeItem(REFRESH_TOKEN_KEY); window.localStorage.removeItem(USER_KEY); window.localStorage.removeItem("token"); window.dispatchEvent(new CustomEvent("chatone-user-changed")); } /** * 续签失败、双 token 均失效等场景:清会话并回到登录页(整页跳转,避免残留状态)。 */ export class SessionExpiredError extends Error { override readonly name = "SessionExpiredError"; constructor(message = "登录已过期,请重新登录") { super(message); Object.setPrototypeOf(this, new.target.prototype); } } export function invalidateSessionAndGoLogin(): void { clearSession(); const base = import.meta.env.BASE_URL || "/"; const loginPath = base === "/" ? "/login" : `${String(base).replace(/\/$/, "")}/login`; window.location.replace(loginPath); } /** * 退出登录:尽量通知后端吊销 refresh(失败则忽略),并清除本地会话。 */ export async function logout(): Promise { const refreshToken = window.localStorage.getItem(REFRESH_TOKEN_KEY); if (refreshToken) { try { await postJson(ApiPath.authLogout, { refreshToken }); } catch { // 无网或接口未实现时仍允许用户本地退出 } } clearSession(); } /** * 发送短信验证码(如登录前获取验证码)。 * @param phone 国际格式,如 `+86153xxxxxxxx` * @param scene 业务场景,默认 `AUTH_SMS_SCENE_LOGIN` * @returns `testCode` 仅部分环境返回,用于联调自动填码 */ export async function sendAuthSmsCode( phone: string, scene: string = AUTH_SMS_SCENE_LOGIN, ): Promise<{ testCode?: string }> { const raw = await postJson(ApiPath.authSmsSend, { phone, scene }); const testCode = raw.testCode; if (typeof testCode === "string" && testCode.trim()) { return { testCode: testCode.trim() }; } return {}; } /** * 短信验证码登录。 * @param phone 国际格式,如 `+86153xxxxxxxx` * @param code 短信验证码 */ export async function smsLogin(phone: string, code: string): Promise { return postJson(ApiPath.authSmsLogin, { phone, code }); } /** * 使用 refreshToken 换取新的访问令牌与刷新令牌。 * 该请求在 `http` 层不会附带旧的 `Authorization`。 */ export async function refreshTokens(refreshToken: string): Promise { return postJson(ApiPath.authRefresh, { refreshToken }); }