Replace custom AuthContext with react-oidc-context
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 <noreply@anthropic.com>
This commit is contained in:
Generated
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="UserContentModel">
|
<component name="UserContentModel">
|
||||||
<attachedFolders />
|
<attachedFolders>
|
||||||
|
<Path>.plans</Path>
|
||||||
|
</attachedFolders>
|
||||||
<explicitIncludes />
|
<explicitIncludes />
|
||||||
<explicitExcludes />
|
<explicitExcludes />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
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 { AuthGuard } from './auth/AuthGuard';
|
||||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
import { ToastProvider } from './components/Toast';
|
import { ToastProvider } from './components/Toast';
|
||||||
import { setTokenProvider } from './api/client';
|
|
||||||
import { CallbackPage } from './pages/CallbackPage';
|
import { CallbackPage } from './pages/CallbackPage';
|
||||||
import { BudgetsPage } from './pages/BudgetsPage';
|
import { BudgetsPage } from './pages/BudgetsPage';
|
||||||
import { IncomePage } from './pages/IncomePage';
|
import { IncomePage } from './pages/IncomePage';
|
||||||
@@ -12,18 +12,18 @@ import { OutgoPage } from './pages/OutgoPage';
|
|||||||
import { SummaryPage } from './pages/SummaryPage';
|
import { SummaryPage } from './pages/SummaryPage';
|
||||||
import { SettingsPage } from './pages/SettingsPage';
|
import { SettingsPage } from './pages/SettingsPage';
|
||||||
|
|
||||||
function TokenWirer() {
|
const onSigninCallback = () => {
|
||||||
const { getToken } = useAuth();
|
if (!window.location.search.includes('error')) {
|
||||||
useEffect(() => { setTokenProvider(getToken); }, [getToken]);
|
window.history.replaceState({}, '', '/');
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<AuthProvider>
|
<AuthProvider {...authConfig} onSigninCallback={onSigninCallback}>
|
||||||
<TokenWirer />
|
<TokenSync />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="/budgets" replace />} />
|
<Route path="/" element={<Navigate to="/budgets" replace />} />
|
||||||
|
|||||||
@@ -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<AuthContextValue | null>(null);
|
|
||||||
|
|
||||||
const userManager = new UserManager(authConfig);
|
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
||||||
const [user, setUser] = useState<User | null>(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 (
|
|
||||||
<AuthContext.Provider value={{ user, isLoading, login, logout, getToken }}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAuth() {
|
|
||||||
const ctx = useContext(AuthContext);
|
|
||||||
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { userManager };
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from 'react-oidc-context';
|
||||||
|
|
||||||
export function AuthGuard({ children }: { children: ReactNode }) {
|
export function AuthGuard({ children }: { children: ReactNode }) {
|
||||||
const { user, isLoading, login } = useAuth();
|
const auth = useAuth();
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>;
|
if (auth.isLoading) return <div>Loading...</div>;
|
||||||
|
|
||||||
if (!user) {
|
if (!auth.isAuthenticated) {
|
||||||
login();
|
auth.signinRedirect();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useEffect } from 'react';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { userManager } from '../auth/AuthContext';
|
|
||||||
|
|
||||||
export function CallbackPage() {
|
export function CallbackPage() {
|
||||||
const navigate = useNavigate();
|
const [params] = useSearchParams();
|
||||||
|
const error = params.get('error');
|
||||||
|
const errorDescription = params.get('error_description');
|
||||||
|
|
||||||
useEffect(() => {
|
if (error) {
|
||||||
userManager.signinRedirectCallback()
|
return (
|
||||||
.then(() => navigate('/budgets'))
|
<div>
|
||||||
.catch(err => {
|
<h2>Sign-in failed</h2>
|
||||||
console.error('OIDC callback error', err);
|
<p>{errorDescription ?? error}</p>
|
||||||
navigate('/');
|
</div>
|
||||||
});
|
);
|
||||||
}, [navigate]);
|
}
|
||||||
|
|
||||||
return <div>Signing in...</div>;
|
return <div>Signing in...</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user