feat: header reflects auth state (account/logout); fix admin api requests losing Content-Type (user delete 422)

This commit is contained in:
flym 2026-09-12 03:25:46 +08:00
parent a06b5f44c4
commit d296b7aa72
2 changed files with 59 additions and 12 deletions

View File

@ -1,14 +1,32 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { useEffect, useState } from "react";
import { CloseIcon, MenuIcon } from "@/components/icons";
import { navigation } from "@/lib/content";
import { getMe, logout } from "@/lib/admin";
import { BrandMark } from "@/components/brand-mark";
import { BrandIcon } from "@/components/brand-icon";
export function SiteHeader() {
const [open, setOpen] = useState(false);
const [authed, setAuthed] = useState(false);
useEffect(() => {
getMe()
.then(() => setAuthed(true))
.catch(() => setAuthed(false));
}, []);
async function handleLogout() {
try {
await logout();
setAuthed(false);
setOpen(false);
} catch {
window.location.href = "/login";
}
}
return (
<header className="sticky top-0 z-50 border-b border-white/5 bg-ink/90 backdrop-blur-xl">
@ -24,9 +42,20 @@ export function SiteHeader() {
))}
</nav>
<div className="desktop-nav items-center gap-6">
{authed ? (
<>
<Link className="button-ghost min-h-10 px-4 py-2 text-sm" href="/account">
<BrandIcon />
</Link>
<button className="button-ghost min-h-10 px-4 py-2 text-sm" onClick={() => void handleLogout()} type="button">
<BrandIcon />退
</button>
</>
) : (
<Link className="button-ghost min-h-10 px-4 py-2 text-sm" href="/login">
<BrandIcon />
</Link>
)}
<Link className="button-primary min-h-10 px-4 py-2 text-sm" href="/contact">
<BrandIcon />
</Link>
@ -43,9 +72,24 @@ export function SiteHeader() {
{item.label}
</Link>
))}
{authed ? (
<>
<Link className="mt-2 rounded-lg border-t border-line px-3 py-3 text-sm text-copy" href="/account" onClick={() => setOpen(false)}>
</Link>
<button
className="mt-1 rounded-lg border-t border-line px-3 py-4 text-left text-sm text-copy hover:bg-panel-raised"
onClick={() => void handleLogout()}
type="button"
>
退
</button>
</>
) : (
<Link className="mt-2 rounded-lg border-t border-line px-3 py-4 text-sm text-copy" href="/login" onClick={() => setOpen(false)}>
/
</Link>
)}
</nav>
</div>
) : null}

View File

@ -28,11 +28,8 @@ export type AdminUserDetail = {
export type Page<T> = { items: T[]; total: number; page: number; limit: number; pages: number };
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const response = await fetch(`${apiBase}${path}`, {
credentials: "include",
headers: { "Content-Type": "application/json", ...(options.headers ?? {}) },
...options,
});
const headers = { "Content-Type": "application/json", ...(options.headers as Record<string, string> | undefined) };
const response = await fetch(`${apiBase}${path}`, { credentials: "include", ...options, headers });
if (response.status === 401 || response.status === 403) {
if (response.status === 403) throw new Error("无权执行此操作");
throw new Error("请登录");
@ -43,6 +40,7 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const msg = Array.isArray(message) ? (message as Array<{ msg?: string }>).map((m) => m.msg ?? "").join("; ") : (message as string);
throw new Error(msg || "请求失败");
}
if (response.status === 204) return undefined as T;
return response.json() as Promise<T>;
}
@ -52,6 +50,11 @@ export async function fetchCsrf(): Promise<string> {
return body.csrf_token ?? "";
}
export async function logout(): Promise<void> {
const csrf = await fetchCsrf();
await request<void>("/auth/logout", { method: "POST", headers: { "X-CSRF-Token": csrf } });
}
export async function getMe(): Promise<{ email: string; role: string; plan: string; status: string }> {
return request("/auth/me");
}