From a26ac6eb7acead0dbf7d92dae6c8f19844a9a19f Mon Sep 17 00:00:00 2001 From: skypank Date: Sun, 21 Jun 2026 15:08:26 +0530 Subject: [PATCH 1/3] feat(frontend): add reusable useUser hook and header auth UX --- application/frontend/src/hooks/index.ts | 1 + application/frontend/src/hooks/useUser.ts | 54 ++++++++++ .../src/scaffolding/Header/Header.tsx | 101 +++++++++--------- 3 files changed, 108 insertions(+), 48 deletions(-) create mode 100644 application/frontend/src/hooks/useUser.ts diff --git a/application/frontend/src/hooks/index.ts b/application/frontend/src/hooks/index.ts index 74aac2712..bc26e9052 100644 --- a/application/frontend/src/hooks/index.ts +++ b/application/frontend/src/hooks/index.ts @@ -1,3 +1,4 @@ export { useEnvironment } from './useEnvironment'; export { useLocationFromOutsideRoute } from './useLocationFromOutsideRoute'; export { useCapabilities } from './useCapabilities'; +export { useUser } from './useUser'; diff --git a/application/frontend/src/hooks/useUser.ts b/application/frontend/src/hooks/useUser.ts new file mode 100644 index 000000000..592d5e392 --- /dev/null +++ b/application/frontend/src/hooks/useUser.ts @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react'; + +import { useEnvironment } from './useEnvironment'; + +export type UserState = { + user: string | null; + isLoggedIn: boolean; + loading: boolean; +}; + +export const useUser = () => { + const { apiUrl } = useEnvironment(); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let active = true; + fetch(`${apiUrl}/user`, { method: 'GET' }) + .then((res) => { + if (res.status === 200) { + return res.text(); + } + return null; // 401 or anything else => treated as not logged in + }) + .then((value) => { + if (active) { + setUser(value && value.trim() !== '' ? value : null); + } + }) + .catch(() => { + if (active) { + setUser(null); // network error => treat as anonymous, do NOT redirect + } + }) + .finally(() => { + if (active) { + setLoading(false); + } + }); + return () => { + active = false; + }; + }, [apiUrl]); + + const login = () => { + window.location.href = `${apiUrl}/login`; + }; + + const logout = () => { + window.location.href = `${apiUrl}/logout`; + }; + + return { user, isLoggedIn: user !== null, loading, login, logout }; +}; diff --git a/application/frontend/src/scaffolding/Header/Header.tsx b/application/frontend/src/scaffolding/Header/Header.tsx index 9f2cd117b..d552b8070 100644 --- a/application/frontend/src/scaffolding/Header/Header.tsx +++ b/application/frontend/src/scaffolding/Header/Header.tsx @@ -1,6 +1,6 @@ import './header.scss'; -import { Menu, Search } from 'lucide-react'; +import { LogOut, Menu, Search, User } from 'lucide-react'; import React, { useEffect, useState } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { NavLink } from 'react-router-dom'; @@ -9,6 +9,7 @@ import { Button } from 'semantic-ui-react'; import { ClearFilterButton } from '../../components/FilterButton/FilterButton'; import { Capabilities } from '../../hooks/useCapabilities'; import { useLocationFromOutsideRoute } from '../../hooks/useLocationFromOutsideRoute'; +import { useUser } from '../../hooks/useUser'; import { SearchBar } from '../../pages/Search/components/SearchBar'; import { ROUTES } from '../../routes'; @@ -26,6 +27,8 @@ export const Header = ({ capabilities }: HeaderProps) => { }; const { showFilter } = useLocationFromOutsideRoute(routes); + const { user, isLoggedIn, loading: userLoading, login, logout } = useUser(); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); useEffect(() => { const mediaQuery = window.matchMedia('(min-width: 768px)'); @@ -100,28 +103,24 @@ export const Header = ({ capabilities }: HeaderProps) => {
- {/* Left these divs so that we can add auth functionality directly here. */} - {/*
- - Log In - - - - Sign Up - -
*/} + {!userLoading && ( +
+ {isLoggedIn ? ( +
+
+ + {user} +
+ +
+ ) : ( +
+ )}
-
- {/*
- - Log In - - - - Sign Up - -
*/} -
+ {!userLoading && ( +
+ {isLoggedIn ? ( +
+
+ + {user} +
+ +
+ ) : ( +
+
+ )} +
+ )} ); From 4afa91f7a292870a29352c0f862b3c114fc3443f Mon Sep 17 00:00:00 2001 From: skypank Date: Sun, 21 Jun 2026 15:20:45 +0530 Subject: [PATCH 2/3] fix(frontend): only map 401 to anonymous; log unexpected /user failures --- application/frontend/src/hooks/useUser.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/application/frontend/src/hooks/useUser.ts b/application/frontend/src/hooks/useUser.ts index 592d5e392..0575fe365 100644 --- a/application/frontend/src/hooks/useUser.ts +++ b/application/frontend/src/hooks/useUser.ts @@ -20,17 +20,25 @@ export const useUser = () => { if (res.status === 200) { return res.text(); } - return null; // 401 or anything else => treated as not logged in + if (res.status === 401) { + return null; // the normal anonymous case — not logged in + } + // Unexpected status (e.g. 5xx): a real failure, not a clean anonymous state. + throw new Error(`Unexpected /user status: ${res.status}`); }) .then((value) => { if (active) { setUser(value && value.trim() !== '' ? value : null); } }) - .catch(() => { + .catch((err) => { + // Network error or unexpected status. Degrade to anonymous so public + // pages stay accessible (never block or redirect), but log the failure + // instead of silently masking it as a normal logged-out state. if (active) { - setUser(null); // network error => treat as anonymous, do NOT redirect + setUser(null); } + console.error('useUser: could not resolve /user login state', err); }) .finally(() => { if (active) { From 13b473a98c3bb481d729f0b6ad5abd21e966f4a0 Mon Sep 17 00:00:00 2001 From: skypank Date: Tue, 23 Jun 2026 16:33:11 +0530 Subject: [PATCH 3/3] feat(frontend): gate header auth UI behind capabilities.login --- application/frontend/src/scaffolding/Header/Header.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/frontend/src/scaffolding/Header/Header.tsx b/application/frontend/src/scaffolding/Header/Header.tsx index d552b8070..e08db2d5a 100644 --- a/application/frontend/src/scaffolding/Header/Header.tsx +++ b/application/frontend/src/scaffolding/Header/Header.tsx @@ -103,7 +103,7 @@ export const Header = ({ capabilities }: HeaderProps) => {
- {!userLoading && ( + {capabilities.login && !userLoading && (
{isLoggedIn ? (
@@ -209,7 +209,7 @@ export const Header = ({ capabilities }: HeaderProps) => { )}
- {!userLoading && ( + {capabilities.login && !userLoading && (
{isLoggedIn ? (