From b33ff5079c1c6af590fe15f11dd7ddbb5561e235 Mon Sep 17 00:00:00 2001 From: Spencer Twaddle <7374698+stwaddle@users.noreply.github.com> Date: Sat, 2 May 2026 16:38:12 -0500 Subject: [PATCH] Replace custom AuthContext with react-oidc-context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes the hand-rolled AuthContext/UserManager setup and replaces it with AuthProvider from react-oidc-context. onSigninCallback clears the OIDC code params from the URL (unless an error is present). TokenSync bridges the library token into the existing api/client setTokenProvider pattern. AuthGuard updated to use auth.isLoading/isAuthenticated/ signinRedirect from the library. CallbackPage simplified to a passive error renderer — react-oidc-context processes the OIDC exchange itself. Co-Authored-By: Claude Sonnet 4.6 --- .idea/.idea.Budget/.idea/indexLayout.xml | 4 +- src/Budget.Client/src/App.tsx | 20 +++---- src/Budget.Client/src/auth/AuthContext.tsx | 58 -------------------- src/Budget.Client/src/auth/AuthGuard.tsx | 10 ++-- src/Budget.Client/src/auth/TokenSync.tsx | 11 ++++ src/Budget.Client/src/pages/CallbackPage.tsx | 24 ++++---- 6 files changed, 41 insertions(+), 86 deletions(-) delete mode 100644 src/Budget.Client/src/auth/AuthContext.tsx create mode 100644 src/Budget.Client/src/auth/TokenSync.tsx diff --git a/.idea/.idea.Budget/.idea/indexLayout.xml b/.idea/.idea.Budget/.idea/indexLayout.xml index 7b08163..17c90aa 100644 --- a/.idea/.idea.Budget/.idea/indexLayout.xml +++ b/.idea/.idea.Budget/.idea/indexLayout.xml @@ -1,7 +1,9 @@ - + + .plans + diff --git a/src/Budget.Client/src/App.tsx b/src/Budget.Client/src/App.tsx index 22d77ed..3756eed 100644 --- a/src/Budget.Client/src/App.tsx +++ b/src/Budget.Client/src/App.tsx @@ -1,10 +1,10 @@ -import { useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { AuthProvider, useAuth } from './auth/AuthContext'; +import { AuthProvider } from 'react-oidc-context'; +import { authConfig } from './auth/authConfig'; +import { TokenSync } from './auth/TokenSync'; import { AuthGuard } from './auth/AuthGuard'; import { ErrorBoundary } from './components/ErrorBoundary'; import { ToastProvider } from './components/Toast'; -import { setTokenProvider } from './api/client'; import { CallbackPage } from './pages/CallbackPage'; import { BudgetsPage } from './pages/BudgetsPage'; import { IncomePage } from './pages/IncomePage'; @@ -12,18 +12,18 @@ import { OutgoPage } from './pages/OutgoPage'; import { SummaryPage } from './pages/SummaryPage'; import { SettingsPage } from './pages/SettingsPage'; -function TokenWirer() { - const { getToken } = useAuth(); - useEffect(() => { setTokenProvider(getToken); }, [getToken]); - return null; -} +const onSigninCallback = () => { + if (!window.location.search.includes('error')) { + window.history.replaceState({}, '', '/'); + } +}; function App() { return ( - - + + } /> diff --git a/src/Budget.Client/src/auth/AuthContext.tsx b/src/Budget.Client/src/auth/AuthContext.tsx deleted file mode 100644 index 3750d87..0000000 --- a/src/Budget.Client/src/auth/AuthContext.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { createContext, useContext, useEffect, useState } from 'react'; -import type { ReactNode } from 'react'; -import { UserManager } from 'oidc-client-ts'; -import type { User } from 'oidc-client-ts'; -import { authConfig } from './authConfig'; - -interface AuthContextValue { - user: User | null; - isLoading: boolean; - login: () => void; - logout: () => void; - getToken: () => string | null; -} - -const AuthContext = createContext(null); - -const userManager = new UserManager(authConfig); - -export function AuthProvider({ children }: { children: ReactNode }) { - const [user, setUser] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - userManager.getUser().then(u => { - setUser(u); - setIsLoading(false); - }); - - const onUserLoaded = (u: User) => setUser(u); - const onUserUnloaded = () => setUser(null); - - userManager.events.addUserLoaded(onUserLoaded); - userManager.events.addUserUnloaded(onUserUnloaded); - - return () => { - userManager.events.removeUserLoaded(onUserLoaded); - userManager.events.removeUserUnloaded(onUserUnloaded); - }; - }, []); - - const login = () => userManager.signinRedirect(); - const logout = () => userManager.signoutRedirect(); - const getToken = () => user?.access_token ?? null; - - return ( - - {children} - - ); -} - -export function useAuth() { - const ctx = useContext(AuthContext); - if (!ctx) throw new Error('useAuth must be used within AuthProvider'); - return ctx; -} - -export { userManager }; diff --git a/src/Budget.Client/src/auth/AuthGuard.tsx b/src/Budget.Client/src/auth/AuthGuard.tsx index 06a6de4..255b8e1 100644 --- a/src/Budget.Client/src/auth/AuthGuard.tsx +++ b/src/Budget.Client/src/auth/AuthGuard.tsx @@ -1,13 +1,13 @@ import type { ReactNode } from 'react'; -import { useAuth } from './AuthContext'; +import { useAuth } from 'react-oidc-context'; export function AuthGuard({ children }: { children: ReactNode }) { - const { user, isLoading, login } = useAuth(); + const auth = useAuth(); - if (isLoading) return
Loading...
; + if (auth.isLoading) return
Loading...
; - if (!user) { - login(); + if (!auth.isAuthenticated) { + auth.signinRedirect(); return null; } diff --git a/src/Budget.Client/src/auth/TokenSync.tsx b/src/Budget.Client/src/auth/TokenSync.tsx new file mode 100644 index 0000000..e924975 --- /dev/null +++ b/src/Budget.Client/src/auth/TokenSync.tsx @@ -0,0 +1,11 @@ +import { useEffect } from 'react'; +import { useAuth } from 'react-oidc-context'; +import { setTokenProvider } from '../api/client'; + +export function TokenSync() { + const auth = useAuth(); + useEffect(() => { + setTokenProvider(() => auth.user?.access_token ?? null); + }, [auth.user]); + return null; +} diff --git a/src/Budget.Client/src/pages/CallbackPage.tsx b/src/Budget.Client/src/pages/CallbackPage.tsx index 0fdd2ab..3b8f733 100644 --- a/src/Budget.Client/src/pages/CallbackPage.tsx +++ b/src/Budget.Client/src/pages/CallbackPage.tsx @@ -1,18 +1,18 @@ -import { useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { userManager } from '../auth/AuthContext'; +import { useSearchParams } from 'react-router-dom'; export function CallbackPage() { - const navigate = useNavigate(); + const [params] = useSearchParams(); + const error = params.get('error'); + const errorDescription = params.get('error_description'); - useEffect(() => { - userManager.signinRedirectCallback() - .then(() => navigate('/budgets')) - .catch(err => { - console.error('OIDC callback error', err); - navigate('/'); - }); - }, [navigate]); + if (error) { + return ( +
+

Sign-in failed

+

{errorDescription ?? error}

+
+ ); + } return
Signing in...
; }