feat: header reflects auth state (account/logout); fix admin api requests losing Content-Type (user delete 422)
This commit is contained in:
parent
a06b5f44c4
commit
d296b7aa72
@ -1,14 +1,32 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { CloseIcon, MenuIcon } from "@/components/icons";
|
import { CloseIcon, MenuIcon } from "@/components/icons";
|
||||||
import { navigation } from "@/lib/content";
|
import { navigation } from "@/lib/content";
|
||||||
|
import { getMe, logout } from "@/lib/admin";
|
||||||
import { BrandMark } from "@/components/brand-mark";
|
import { BrandMark } from "@/components/brand-mark";
|
||||||
import { BrandIcon } from "@/components/brand-icon";
|
import { BrandIcon } from "@/components/brand-icon";
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
const [open, setOpen] = useState(false);
|
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 (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-white/5 bg-ink/90 backdrop-blur-xl">
|
<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>
|
</nav>
|
||||||
<div className="desktop-nav items-center gap-6">
|
<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">
|
<Link className="button-ghost min-h-10 px-4 py-2 text-sm" href="/login">
|
||||||
<BrandIcon />登录
|
<BrandIcon />登录
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
<Link className="button-primary min-h-10 px-4 py-2 text-sm" href="/contact">
|
<Link className="button-primary min-h-10 px-4 py-2 text-sm" href="/contact">
|
||||||
<BrandIcon />联系我们
|
<BrandIcon />联系我们
|
||||||
</Link>
|
</Link>
|
||||||
@ -43,9 +72,24 @@ export function SiteHeader() {
|
|||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</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 className="mt-2 rounded-lg border-t border-line px-3 py-4 text-sm text-copy" href="/login" onClick={() => setOpen(false)}>
|
||||||
登录 / 账户入口
|
登录 / 账户入口
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
13
lib/admin.ts
13
lib/admin.ts
@ -28,11 +28,8 @@ export type AdminUserDetail = {
|
|||||||
export type Page<T> = { items: T[]; total: number; page: number; limit: number; pages: number };
|
export type Page<T> = { items: T[]; total: number; page: number; limit: number; pages: number };
|
||||||
|
|
||||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
const response = await fetch(`${apiBase}${path}`, {
|
const headers = { "Content-Type": "application/json", ...(options.headers as Record<string, string> | undefined) };
|
||||||
credentials: "include",
|
const response = await fetch(`${apiBase}${path}`, { credentials: "include", ...options, headers });
|
||||||
headers: { "Content-Type": "application/json", ...(options.headers ?? {}) },
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
if (response.status === 403) throw new Error("无权执行此操作");
|
if (response.status === 403) throw new Error("无权执行此操作");
|
||||||
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);
|
const msg = Array.isArray(message) ? (message as Array<{ msg?: string }>).map((m) => m.msg ?? "").join("; ") : (message as string);
|
||||||
throw new Error(msg || "请求失败");
|
throw new Error(msg || "请求失败");
|
||||||
}
|
}
|
||||||
|
if (response.status === 204) return undefined as T;
|
||||||
return response.json() as Promise<T>;
|
return response.json() as Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +50,11 @@ export async function fetchCsrf(): Promise<string> {
|
|||||||
return body.csrf_token ?? "";
|
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 }> {
|
export async function getMe(): Promise<{ email: string; role: string; plan: string; status: string }> {
|
||||||
return request("/auth/me");
|
return request("/auth/me");
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user