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..0575fe365 --- /dev/null +++ b/application/frontend/src/hooks/useUser.ts @@ -0,0 +1,62 @@ +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(); + } + 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((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); + } + console.error('useUser: could not resolve /user login state', err); + }) + .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..e08db2d5a 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 - -
*/} + {capabilities.login && !userLoading && ( +
+ {isLoggedIn ? ( +
+
+ + {user} +
+ +
+ ) : ( +
+ )}
-
- {/*
- - Log In - - - - Sign Up - -
*/} -
+ {capabilities.login && !userLoading && ( +
+ {isLoggedIn ? ( +
+
+ + {user} +
+ +
+ ) : ( +
+
+ )} +
+ )} );