diff --git a/src/app/panel/components/Header.tsx b/src/app/panel/components/Header.tsx index ab70b4d..9495cde 100644 --- a/src/app/panel/components/Header.tsx +++ b/src/app/panel/components/Header.tsx @@ -1,7 +1,9 @@ 'use client' import Link from 'next/link' +import { useRouter } from 'next/navigation' import { ChevronLeft, ChevronRight, Menu } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' import { useUserStore } from '@lib/userStore' import type { UserRole } from '@lib/userStore' @@ -48,6 +50,7 @@ function resolveAccountInitial(input?: string | null) { export default function Header({ onMenu, onCollapse, isCollapsed }: HeaderProps) { const { language } = useLanguage() + const router = useRouter() const user = useUserStore((state) => state.user) const isLoading = useUserStore((state) => state.isLoading) const role: UserRole = user?.role ?? 'guest' @@ -59,8 +62,104 @@ export default function Header({ onMenu, onCollapse, isCollapsed }: HeaderProps) ? 'bg-[var(--color-surface-muted)] text-[var(--color-text-subtle)] opacity-70' : badge.className + const isRoot = useMemo(() => { + const email = user?.email?.trim().toLowerCase() ?? '' + return email === 'admin@svc.plus' && role === 'admin' + }, [role, user?.email]) + + const [assumeStatus, setAssumeStatus] = useState<{ isAssuming: boolean; target?: string }>({ + isAssuming: false, + }) + const [assumeBusy, setAssumeBusy] = useState(false) + + useEffect(() => { + let cancelled = false + void (async () => { + try { + const res = await fetch('/api/sandbox/assume/status', { method: 'GET', cache: 'no-store' }) + const payload = (await res.json().catch(() => null)) as any + if (cancelled) return + setAssumeStatus({ + isAssuming: Boolean(payload?.isAssuming), + target: typeof payload?.target === 'string' ? payload.target : undefined, + }) + } catch { + if (cancelled) return + setAssumeStatus({ isAssuming: false }) + } + })() + return () => { + cancelled = true + } + }, []) + + const handleAssumeSandbox = async () => { + if (!isRoot || assumeBusy) return + try { + setAssumeBusy(true) + const res = await fetch('/api/sandbox/assume', { method: 'POST', cache: 'no-store', credentials: 'include' }) + if (!res.ok) { + const payload = (await res.json().catch(() => null)) as any + throw new Error((payload && (payload.message || payload.error)) || `Assume failed (${res.status})`) + } + router.refresh() + // Ensure server-rendered parts reflect the new cookie immediately. + window.location.reload() + } finally { + setAssumeBusy(false) + } + } + + const handleRevertAssume = async () => { + if (assumeBusy) return + try { + setAssumeBusy(true) + const res = await fetch('/api/sandbox/assume/revert', { method: 'POST', cache: 'no-store', credentials: 'include' }) + if (!res.ok) { + const payload = (await res.json().catch(() => null)) as any + throw new Error((payload && (payload.message || payload.error)) || `Revert failed (${res.status})`) + } + router.refresh() + window.location.reload() + } finally { + setAssumeBusy(false) + } + } + return ( -
+
+ {assumeStatus.isAssuming ? ( +
+
+ {language === 'zh' + ? `当前处于 Assume: ${assumeStatus.target || 'sandbox@svc.plus'}(只读视角)` + : `Assuming: ${assumeStatus.target || 'sandbox@svc.plus'} (read-only view)`} +
+ +
+ ) : ( +
+ {isRoot ? ( + + ) : null} +
+ )} + +
+
) }