You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

113 lines
3.1 KiB

import { request, unwrapApiBody, type ApiResponse } from "../utils/http/factory";
export type AuthUser = {
id: number;
username: string;
role: string;
publicSlug: string | null;
nickname: string | null;
avatar: string | null;
};
type MeResult = {
user: AuthUser;
};
type SessionResult = {
user: AuthUser | null;
};
export type AuthSessionState = {
initialized: boolean;
pending: boolean;
loggedIn: boolean;
user: AuthUser | null;
};
export const AUTH_SESSION_STATE_KEY = "auth:session";
/** 客户端是否已做过与 Cookie 的一次强制对齐(`ensureClientMeSynced`);登出等场景需重置 */
export const AUTH_CLIENT_ME_SYNCED_KEY = "auth:client-me-synced";
export const DEFAULT_AUTH_SESSION_STATE: AuthSessionState = {
initialized: false,
pending: false,
loggedIn: false,
user: null,
};
function isUnauthorized(error: unknown) {
if (typeof error !== "object" || error === null) {
return false;
}
return "statusCode" in error && (error as { statusCode?: number }).statusCode === 401;
}
export function useAuthSession() {
const state = useState<AuthSessionState>(AUTH_SESSION_STATE_KEY, () => ({
...DEFAULT_AUTH_SESSION_STATE,
}));
const clientMeSynced = useState<boolean>(AUTH_CLIENT_ME_SYNCED_KEY, () => false);
const applyUser = (user: AuthUser | null) => {
state.value.user = user;
state.value.loggedIn = Boolean(user);
state.value.initialized = true;
};
const clear = () => {
clientMeSynced.value = false;
applyUser(null);
};
const refresh = async (force = false) => {
if (state.value.initialized && !force) {
return state.value.user;
}
if (state.value.pending) {
return state.value.user;
}
state.value.pending = true;
try {
const fetcher = import.meta.server ? useRequestFetch() : request;
const payload = await fetcher<ApiResponse<MeResult>>("/api/auth/me");
const data = unwrapApiBody(payload);
applyUser(data.user);
return data.user;
} catch (error: unknown) {
if (isUnauthorized(error)) {
clear();
return null;
}
throw error;
} finally {
state.value.pending = false;
}
};
/**
* 全站 SPA 生命周期内至多同步一次客户端会话(`GET /api/auth/session`)。
* 无会话 Cookie 时服务端不查库;有 Cookie 时与 `/api/auth/me` 相比少一次额外查询。
*/
const ensureClientMeSynced = async (): Promise<void> => {
if (import.meta.server || clientMeSynced.value) {
return;
}
try {
const payload = await request<ApiResponse<SessionResult>>("/api/auth/session");
const data = unwrapApiBody(payload);
applyUser(data.user);
clientMeSynced.value = true;
} catch {
/* 网络错误等:保留 SSR 状态,下次进入带壳页面可再试 */
}
};
return {
initialized: computed(() => state.value.initialized),
loggedIn: computed(() => state.value.loggedIn),
user: computed(() => state.value.user),
pending: computed(() => state.value.pending),
refresh,
clear,
ensureClientMeSynced,
};
}