From 512dcbf0abc85769929d768cebee30a8bd84a47a Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Sat, 22 Aug 2026 20:45:57 +0100 Subject: [PATCH 1/2] auth system init --- frontend/src/App.tsx | 39 ++++++++++++----------- frontend/src/auth/AuthContext.ts | 20 ++++++++++++ frontend/src/auth/AuthProvider.tsx | 46 ++++++++++++++++++++++++++++ frontend/src/auth/useAuth.ts | 14 +++++++++ frontend/src/components/Sidebar.tsx | 7 +++-- frontend/src/components/Topbar.tsx | 17 ++-------- frontend/src/layouts/MainLayout.tsx | 23 ++------------ frontend/src/pages/Profile.tsx | 11 ++----- frontend/src/pages/StreamControl.tsx | 10 ++---- 9 files changed, 117 insertions(+), 70 deletions(-) create mode 100644 frontend/src/auth/AuthContext.ts create mode 100644 frontend/src/auth/AuthProvider.tsx create mode 100644 frontend/src/auth/useAuth.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e427aea..9d7bbac 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,6 +17,7 @@ import StreamControl from "./pages/StreamControl.tsx"; import { GoogleOAuthProvider } from '@react-oauth/google'; import {config} from "./config.ts"; import NotFound from "./pages/NotFound.tsx"; +import AuthProvider from "./auth/AuthProvider.tsx"; function App() { @@ -26,24 +27,26 @@ function App() { return ( - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); } diff --git a/frontend/src/auth/AuthContext.ts b/frontend/src/auth/AuthContext.ts new file mode 100644 index 0000000..9684f2e --- /dev/null +++ b/frontend/src/auth/AuthContext.ts @@ -0,0 +1,20 @@ +import { createContext } from "react"; +import type { User } from "../utils/types.ts"; + +type AuthContextValue = { + /** The currently authenticated user, or null when logged out. */ + user: User | null; + /** True until the initial "who am I" request has resolved. */ + loading: boolean; + /** Exchange a Google credential for a session, then load the user. */ + login: (googleCredential: string) => Promise; + /** End the session and clear the user. */ + logout: () => Promise; + /** Re-fetch the current user (e.g. after a profile change). */ + refresh: () => Promise; +}; + +const AuthContext = createContext(null); + +export { AuthContext }; +export type { AuthContextValue }; diff --git a/frontend/src/auth/AuthProvider.tsx b/frontend/src/auth/AuthProvider.tsx new file mode 100644 index 0000000..1091794 --- /dev/null +++ b/frontend/src/auth/AuthProvider.tsx @@ -0,0 +1,46 @@ +import { useCallback, useEffect, useState } from "react"; +import type { ReactNode } from "react"; +import type { User } from "../utils/types.ts"; +import { login as apiLogin, logout as apiLogout, getUser } from "../utils/api/users.ts"; +import { AuthContext } from "./AuthContext.ts"; + +/** + * Owns the single source of truth for the authenticated user. Everything that + * needs to know who is logged in — the layout, the top bar, protected pages — + * reads it through the `useAuth` hook rather than fetching the user itself. + */ +const AuthProvider = ({ children }: { children: ReactNode }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const refresh = useCallback(async () => { + try { + setUser(await getUser()); + } catch (error) { + console.error("Failed to fetch user:", error); + setUser(null); + } + }, []); + + useEffect(() => { + refresh().finally(() => setLoading(false)); + }, [refresh]); + + const login = useCallback(async (googleCredential: string) => { + await apiLogin(googleCredential); + await refresh(); + }, [refresh]); + + const logout = useCallback(async () => { + await apiLogout(); + setUser(null); + }, []); + + return ( + + {children} + + ); +}; + +export default AuthProvider; diff --git a/frontend/src/auth/useAuth.ts b/frontend/src/auth/useAuth.ts new file mode 100644 index 0000000..2745709 --- /dev/null +++ b/frontend/src/auth/useAuth.ts @@ -0,0 +1,14 @@ +import { useContext } from "react"; +import { AuthContext } from "./AuthContext.ts"; +import type { AuthContextValue } from "./AuthContext.ts"; + +/** Access the centralised auth state. Must be used within an . */ +const useAuth = (): AuthContextValue => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; + +export { useAuth }; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 5a8276b..48928df 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,16 +1,17 @@ import clsx from "clsx"; import SidebarButton from "./buttons/SidebarButton.tsx"; import { Plus, Film, Home, User as UserIcon, Video, Radio } from 'lucide-react'; -import type {User} from "../utils/types.ts"; +import { useAuth } from "../auth/useAuth.ts"; type props = { - user: User | null; className?: string } const Divider = () =>
; -const Sidebar = ({user, className}: props) => { +const Sidebar = ({className}: props) => { + const { user } = useAuth(); + return (