101 lines
2.5 KiB
TypeScript
101 lines
2.5 KiB
TypeScript
import { getStoredToken, getStoredUser } from "@/app/lib/pocketbase";
|
||
|
||
export const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
|
||
|
||
export function apiUrl(path: string): string {
|
||
if (/^https?:\/\//i.test(path)) return path;
|
||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||
if (API_BASE.endsWith("/api") && normalizedPath.startsWith("/api/")) {
|
||
return `${API_BASE}${normalizedPath.slice(4)}`;
|
||
}
|
||
return `${API_BASE}${normalizedPath}`;
|
||
}
|
||
|
||
export async function apiFetch<T>(
|
||
path: string,
|
||
init?: RequestInit
|
||
): Promise<T> {
|
||
const res = await fetch(apiUrl(path), {
|
||
credentials: "include",
|
||
...init,
|
||
headers:
|
||
init?.body instanceof FormData
|
||
? init.headers
|
||
: {
|
||
"Content-Type": "application/json",
|
||
...(init?.headers || {}),
|
||
},
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) {
|
||
throw new Error(data?.detail || data?.error || `HTTP ${res.status}`);
|
||
}
|
||
return data as T;
|
||
}
|
||
|
||
export type AuthUser = {
|
||
id: string;
|
||
email: string;
|
||
vip?: boolean;
|
||
role?: string;
|
||
[key: string]: unknown;
|
||
};
|
||
|
||
export type AuthMeResponse = {
|
||
user?: AuthUser | null;
|
||
token?: string;
|
||
};
|
||
|
||
/** 读取当前登录用户;失败时返回 null,不抛错 */
|
||
export async function fetchAuthMe(): Promise<AuthMeResponse | null> {
|
||
try {
|
||
return await apiFetch<AuthMeResponse>("/api/auth/me", { cache: "no-store" });
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** 将本地 PocketBase token 同步到 httpOnly cookie */
|
||
export async function syncAuthSession(
|
||
token: string,
|
||
record: Record<string, unknown>
|
||
): Promise<boolean> {
|
||
try {
|
||
await apiFetch("/api/auth/sync-session", {
|
||
method: "POST",
|
||
body: JSON.stringify({ token, record }),
|
||
});
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析当前用户:优先 cookie 会话,其次本地 token 同步后再读。
|
||
* 返回 user + token(若有),未登录返回 null。
|
||
*/
|
||
export async function resolveAuthUser(): Promise<{
|
||
user: AuthUser;
|
||
token: string;
|
||
} | null> {
|
||
let data = await fetchAuthMe();
|
||
if (data?.user?.id && data?.token) {
|
||
return { user: data.user, token: data.token };
|
||
}
|
||
|
||
const token = getStoredToken();
|
||
const record = getStoredUser();
|
||
if (token && record?.id && record?.email) {
|
||
const synced = await syncAuthSession(token, record as Record<string, unknown>);
|
||
if (synced) {
|
||
data = await fetchAuthMe();
|
||
if (data?.user?.id && data?.token) {
|
||
return { user: data.user, token: data.token };
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|