feat: rebuild CSP practice workflow, UX and automation

这个提交包含在:
Codex CLI
2026-02-13 15:49:05 +08:00
父节点 d33deed4c5
当前提交 e2ab522b78
修改 105 个文件,包含 15669 行新增428 行删除

54
frontend/src/lib/api.ts 普通文件
查看文件

@@ -0,0 +1,54 @@
export const API_BASE =
process.env.NEXT_PUBLIC_API_BASE ??
(process.env.NODE_ENV === "development" ? "http://localhost:8080" : "/admin139");
type ApiEnvelope<T> =
| { ok: true; data?: T; [k: string]: unknown }
| { ok: false; error?: string; [k: string]: unknown };
export async function apiFetch<T>(
path: string,
init?: RequestInit,
token?: string
): Promise<T> {
const headers = new Headers(init?.headers);
if (token) headers.set("Authorization", `Bearer ${token}`);
if (init?.body && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
const resp = await fetch(`${API_BASE}${path}`, {
...init,
headers,
cache: "no-store",
});
const text = await resp.text();
let payload: unknown = null;
if (text) {
try {
payload = JSON.parse(text) as unknown;
} catch {
payload = text;
}
}
if (!resp.ok) {
const msg =
typeof payload === "object" && payload !== null && "error" in payload
? String((payload as { error?: unknown }).error ?? `HTTP ${resp.status}`)
: `HTTP ${resp.status}`;
throw new Error(msg);
}
if (typeof payload === "object" && payload !== null && "ok" in payload) {
const env = payload as ApiEnvelope<T>;
if (!env.ok) {
throw new Error(env.error ?? "request failed");
}
if ("data" in env) return (env.data as T) ?? ({} as T);
return payload as T;
}
return payload as T;
}

16
frontend/src/lib/auth.ts 普通文件
查看文件

@@ -0,0 +1,16 @@
const TOKEN_KEY = "csp_token";
export function readToken(): string {
if (typeof window === "undefined") return "";
return window.localStorage.getItem(TOKEN_KEY) ?? "";
}
export function saveToken(token: string): void {
if (typeof window === "undefined") return;
window.localStorage.setItem(TOKEN_KEY, token);
}
export function clearToken(): void {
if (typeof window === "undefined") return;
window.localStorage.removeItem(TOKEN_KEY);
}