From 575fbdb6de5f09269afd19aea09b5f4d64eb9598 Mon Sep 17 00:00:00 2001 From: Godopu Date: Tue, 25 Aug 2026 15:08:40 +0900 Subject: [PATCH] fix(console,api): direct render AdminLoginForm on unauthenticated and harden URL normalization - 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 --- .../console/_components/AdminLoginForm.tsx | 82 +++++++++++++++++++ refer_landing_page/app/console/layout.tsx | 47 ++++++----- refer_landing_page/app/console/login/page.tsx | 76 +---------------- refer_landing_page/lib/adminApi.ts | 11 ++- refer_landing_page/lib/api.ts | 11 ++- 5 files changed, 122 insertions(+), 105 deletions(-) create mode 100644 refer_landing_page/app/console/_components/AdminLoginForm.tsx diff --git a/refer_landing_page/app/console/_components/AdminLoginForm.tsx b/refer_landing_page/app/console/_components/AdminLoginForm.tsx new file mode 100644 index 0000000..f489300 --- /dev/null +++ b/refer_landing_page/app/console/_components/AdminLoginForm.tsx @@ -0,0 +1,82 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { setClientToken } from "./AdminComponents"; +import { adminVerifyToken } from "../../../lib/adminApi"; + +export function AdminLoginForm({ onLoginSuccess }: { onLoginSuccess?: () => void }) { + const router = useRouter(); + const [token, setToken] = useState(""); + const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!token.trim()) { + setError("Please enter the admin token."); + return; + } + + setIsSubmitting(true); + setError(null); + + try { + const isValid = await adminVerifyToken(token.trim()); + if (isValid) { + setClientToken(token.trim()); + if (onLoginSuccess) { + onLoginSuccess(); + } else { + router.push("/console"); + } + } else { + setError("Invalid admin bearer token. Please check your credentials."); + } + } catch (err: any) { + setError(err?.message || "Failed to verify token with backend API."); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+

ANL Admin Console

+

Enter your admin bearer token to manage laboratory datasets.

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setToken(e.target.value)} + placeholder="e.g. admin123" + className="w-full px-3 py-2 border border-line rounded-md bg-ivory text-ink focus:outline-none focus:ring-2 focus:ring-cobalt/50" + required + /> +
+ + +
+
+ ); +} diff --git a/refer_landing_page/app/console/layout.tsx b/refer_landing_page/app/console/layout.tsx index b68a9f9..bd0eca5 100644 --- a/refer_landing_page/app/console/layout.tsx +++ b/refer_landing_page/app/console/layout.tsx @@ -4,6 +4,7 @@ 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 }) { @@ -15,15 +16,9 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) let isMounted = true; async function checkAuth() { - if (pathname === "/console/login") { - if (isMounted) setIsAuthenticated(false); - return; - } - const token = getClientToken(); if (!token) { if (isMounted) setIsAuthenticated(false); - router.replace("/console/login"); return; } @@ -33,7 +28,6 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) if (!isValid) { removeClientToken(); setIsAuthenticated(false); - router.replace("/console/login"); } else { setIsAuthenticated(true); } @@ -43,36 +37,41 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) return () => { isMounted = false; }; - }, [pathname, router]); + }, [pathname]); if (pathname === "/console/login") { - return
{children}
; + return ( +
+ { + setIsAuthenticated(true); + router.push("/console"); + }} + /> +
+ ); } if (isAuthenticated === null) { + const token = typeof window !== "undefined" ? getClientToken() : null; + if (!token) { + return ( +
+ setIsAuthenticated(true)} /> +
+ ); + } return (
Checking authentication...
- - Click here to sign in -
); } if (isAuthenticated === false) { return ( -
-
Redirecting to login...
- - Go to Sign In Page - +
+ setIsAuthenticated(true)} />
); } @@ -89,7 +88,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) const handleLogout = () => { removeClientToken(); - router.push("/console/login"); + setIsAuthenticated(false); }; return ( diff --git a/refer_landing_page/app/console/login/page.tsx b/refer_landing_page/app/console/login/page.tsx index f4cee52..cbfff45 100644 --- a/refer_landing_page/app/console/login/page.tsx +++ b/refer_landing_page/app/console/login/page.tsx @@ -1,78 +1,8 @@ "use client"; -import React, { useState } from "react"; -import { useRouter } from "next/navigation"; -import { setClientToken } from "../_components/AdminComponents"; -import { adminVerifyToken } from "../../../lib/adminApi"; +import React from "react"; +import { AdminLoginForm } from "../_components/AdminLoginForm"; export default function AdminLoginPage() { - const router = useRouter(); - const [token, setToken] = useState(""); - const [error, setError] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!token.trim()) { - setError("Please enter the admin token."); - return; - } - - setIsSubmitting(true); - setError(null); - - try { - const isValid = await adminVerifyToken(token.trim()); - if (isValid) { - setClientToken(token.trim()); - router.push("/console"); - } else { - setError("Invalid admin bearer token. Please check your credentials."); - } - } catch (err: any) { - setError(err?.message || "Failed to verify token with backend API."); - } finally { - setIsSubmitting(false); - } - }; - - return ( -
-
-

ANL Admin Console

-

Enter your admin bearer token to manage laboratory datasets.

-
- - {error && ( -
- {error} -
- )} - -
-
- - setToken(e.target.value)} - placeholder="e.g. admin123" - className="w-full px-3 py-2 border border-line rounded-md bg-ivory text-ink focus:outline-none focus:ring-2 focus:ring-cobalt/50" - required - /> -
- - -
-
- ); + return ; } diff --git a/refer_landing_page/lib/adminApi.ts b/refer_landing_page/lib/adminApi.ts index 021a804..1dc034c 100644 --- a/refer_landing_page/lib/adminApi.ts +++ b/refer_landing_page/lib/adminApi.ts @@ -5,13 +5,16 @@ export function normalizeApiUrl(rawUrl?: string): string { let url = (rawUrl || "").trim().replace(/\/+$/, ""); - if (!url) { + if (!url || url === "/" || url === "/api" || url === "/api/v1") { return "/api/v1"; } - if (!url.endsWith("/api/v1")) { - url = `${url}/api/v1`; + if (url.endsWith("/api/v1")) { + return url; } - return url; + if (url.endsWith("/api")) { + return `${url}/v1`; + } + return `${url}/api/v1`; } export function getAdminApiBaseUrl(): string { diff --git a/refer_landing_page/lib/api.ts b/refer_landing_page/lib/api.ts index e439384..e95869d 100644 --- a/refer_landing_page/lib/api.ts +++ b/refer_landing_page/lib/api.ts @@ -12,13 +12,16 @@ import type { export function normalizeApiUrl(rawUrl?: string): string { let url = (rawUrl || "").trim().replace(/\/+$/, ""); - if (!url) { + if (!url || url === "/" || url === "/api" || url === "/api/v1") { return "/api/v1"; } - if (!url.endsWith("/api/v1")) { - url = `${url}/api/v1`; + if (url.endsWith("/api/v1")) { + return url; } - return url; + if (url.endsWith("/api")) { + return `${url}/v1`; + } + return `${url}/api/v1`; } export function getApiBaseUrl(): string {