All checks were successful
CI / build (push) Successful in 1m59s
新增会话与消息查询 API,并将首页改为真实会话驱动;当前选中会话会同步到 URL 参数,刷新或直达链接可恢复上下文。 Made-with: Cursor
142 lines
4.4 KiB
TypeScript
142 lines
4.4 KiB
TypeScript
/**
|
||
* 鉴权相关:登录、刷新令牌、本地会话读写。
|
||
*
|
||
* 会话默认存 `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<string, unknown>;
|
||
|
||
/** `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<void> {
|
||
const refreshToken = window.localStorage.getItem(REFRESH_TOKEN_KEY);
|
||
if (refreshToken) {
|
||
try {
|
||
await postJson<unknown>(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<SmsSendResponse>(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<SmsLoginResponse> {
|
||
return postJson<SmsLoginResponse>(ApiPath.authSmsLogin, { phone, code });
|
||
}
|
||
|
||
/**
|
||
* 使用 refreshToken 换取新的访问令牌与刷新令牌。
|
||
* 该请求在 `http` 层不会附带旧的 `Authorization`。
|
||
*/
|
||
export async function refreshTokens(refreshToken: string): Promise<RefreshTokenResponse> {
|
||
return postJson<RefreshTokenResponse>(ApiPath.authRefresh, { refreshToken });
|
||
}
|