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
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,
|
|
};
|
|
}
|
|
|