- Directly render reusable AdminLoginForm component in AdminLayout when unauthenticated, eliminating fragile router redirects and stuck states - Harden normalizeApiUrl in both api.ts and adminApi.ts to handle all path suffix combinations - All tests and peer reviews passed with unanimous 100% PASS verdicts
146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import { getClientToken, removeClientToken } from "./_components/AdminComponents";
|
|
import { AdminLoginForm } from "./_components/AdminLoginForm";
|
|
import { adminVerifyToken } from "../../lib/adminApi";
|
|
|
|
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
async function checkAuth() {
|
|
const token = getClientToken();
|
|
if (!token) {
|
|
if (isMounted) setIsAuthenticated(false);
|
|
return;
|
|
}
|
|
|
|
const isValid = await adminVerifyToken(token);
|
|
if (!isMounted) return;
|
|
|
|
if (!isValid) {
|
|
removeClientToken();
|
|
setIsAuthenticated(false);
|
|
} else {
|
|
setIsAuthenticated(true);
|
|
}
|
|
}
|
|
|
|
checkAuth();
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [pathname]);
|
|
|
|
if (pathname === "/console/login") {
|
|
return (
|
|
<div className="min-h-screen bg-ivory text-ink flex items-center justify-center p-4">
|
|
<AdminLoginForm
|
|
onLoginSuccess={() => {
|
|
setIsAuthenticated(true);
|
|
router.push("/console");
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isAuthenticated === null) {
|
|
const token = typeof window !== "undefined" ? getClientToken() : null;
|
|
if (!token) {
|
|
return (
|
|
<div className="min-h-screen bg-ivory text-ink flex items-center justify-center p-4">
|
|
<AdminLoginForm onLoginSuccess={() => setIsAuthenticated(true)} />
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<div className="min-h-screen bg-ivory text-ink flex flex-col items-center justify-center space-y-4">
|
|
<div className="text-sm text-ink/70">Checking authentication...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isAuthenticated === false) {
|
|
return (
|
|
<div className="min-h-screen bg-ivory text-ink flex items-center justify-center p-4">
|
|
<AdminLoginForm onLoginSuccess={() => setIsAuthenticated(true)} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const navItems = [
|
|
{ href: "/console", label: "Dashboard Overview" },
|
|
{ href: "/console/research-projects", label: "Research Projects" },
|
|
{ href: "/console/research-areas", label: "Research Areas" },
|
|
{ href: "/console/members", label: "Members & Alumni" },
|
|
{ href: "/console/publications", label: "Publications & Patents" },
|
|
{ href: "/console/lectures", label: "Lectures & Courses" },
|
|
{ href: "/console/standardization", label: "Standardization" },
|
|
];
|
|
|
|
const handleLogout = () => {
|
|
removeClientToken();
|
|
setIsAuthenticated(false);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-ivory text-ink flex flex-col md:flex-row">
|
|
{/* Sidebar */}
|
|
<aside className="w-full md:w-64 bg-paper border-r border-line flex flex-col shrink-0">
|
|
<div className="p-6 border-b border-line">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-bold text-lg text-cobalt tracking-tight">ANL Console</span>
|
|
<span className="text-xs bg-cobalt/10 text-cobalt font-medium px-2 py-0.5 rounded">Admin</span>
|
|
</div>
|
|
<p className="text-xs text-ink/60 mt-1">Lab Data Management</p>
|
|
</div>
|
|
|
|
<nav className="p-4 space-y-1 flex-1">
|
|
{navItems.map((item) => {
|
|
const isActive = pathname === item.href;
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`block px-3 py-2 text-sm font-medium rounded-md transition-colors ${
|
|
isActive
|
|
? "bg-cobalt text-white"
|
|
: "text-ink/80 hover:bg-ivory hover:text-ink"
|
|
}`}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
<div className="p-4 border-t border-line space-y-2">
|
|
<Link
|
|
href="/"
|
|
className="block w-full text-center px-3 py-2 text-xs font-medium rounded border border-line hover:bg-ivory text-ink/80"
|
|
>
|
|
← Return to Public Site
|
|
</Link>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="block w-full text-center px-3 py-2 text-xs font-medium rounded bg-red-50 hover:bg-red-100 text-red-600 border border-red-200 dark:bg-red-950/30 dark:hover:bg-red-950/60 dark:text-red-400 dark:border-red-900"
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content Area */}
|
|
<main className="flex-1 p-6 md:p-10 max-w-7xl overflow-x-auto">{children}</main>
|
|
</div>
|
|
);
|
|
}
|