Compare commits

..

4 Commits

Author SHA1 Message Date
Eric Gullickson
15128bfd50 fix: add missing hook dependencies for stale token effect (refs #190)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 21:57:28 -06:00
Eric Gullickson
723e25e1a7 fix: add pre-auth session clear mechanism on HomePage (refs #192)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 21:56:24 -06:00
Eric Gullickson
6e493e9bc7 fix: detect and clear stale IndexedDB auth tokens (refs #190)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 21:55:54 -06:00
Eric Gullickson
a195fa9231 fix: allow callback route to complete Auth0 code exchange (refs #189)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 21:55:24 -06:00
4 changed files with 103 additions and 12 deletions

View File

@@ -557,18 +557,37 @@ function App() {
);
}
// Callback route requires authentication - handled by CallbackPage component
if (isCallbackRoute && isAuthenticated) {
if (isCallbackRoute) {
if (isAuthenticated) {
return (
<ThemeProvider>
<React.Suspense fallback={
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Processing login...</div>
</div>
}>
{mobileMode ? <CallbackMobileScreen /> : <CallbackPage />}
</React.Suspense>
<DebugInfo />
</ThemeProvider>
);
}
if (mobileMode) {
return (
<ThemeProvider>
<Layout mobileMode={true}>
<div className="flex items-center justify-center h-64">
<div className="text-slate-500">Processing login...</div>
</div>
</Layout>
</ThemeProvider>
);
}
return (
<ThemeProvider>
<React.Suspense fallback={
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Processing login...</div>
</div>
}>
{mobileMode ? <CallbackMobileScreen /> : <CallbackPage />}
</React.Suspense>
<DebugInfo />
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Processing login...</div>
</div>
</ThemeProvider>
);
}

View File

@@ -57,8 +57,9 @@ export const Auth0Provider: React.FC<Auth0ProviderProps> = ({ children }) => {
// Component to inject token into API client with mobile support
const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { getAccessTokenSilently, isAuthenticated, isLoading, user } = useAuth0();
const { getAccessTokenSilently, isAuthenticated, isLoading, user, logout } = useAuth0();
const [retryCount, setRetryCount] = React.useState(0);
const validatingRef = React.useRef(false);
// Basic component loading debug
console.log('[TokenInjector] Component loaded');
@@ -116,6 +117,31 @@ const TokenInjector: React.FC<{ children: React.ReactNode }> = ({ children }) =>
return null;
};
// Prevent stale session state when cached token is no longer valid
React.useEffect(() => {
if (!isAuthenticated || isLoading || validatingRef.current) return;
const validateToken = async () => {
validatingRef.current = true;
try {
await getAccessTokenSilently({ cacheMode: 'off', timeoutInSeconds: 10 });
} catch (error: any) {
const errorType = error?.error || error?.message || '';
if (errorType.includes('login_required') || errorType.includes('consent_required') ||
errorType.includes('invalid_grant')) {
console.warn('[Auth] Stale token detected, clearing auth state');
const { indexedDBStorage } = await import('../utils/indexeddb-storage');
await indexedDBStorage.clearAll();
logout({ openUrl: false });
return;
}
}
validatingRef.current = false;
};
validateToken();
}, [isAuthenticated, isLoading, getAccessTokenSilently, logout]);
// Force authentication check for devices when user seems logged in but isAuthenticated is false
React.useEffect(() => {
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

View File

@@ -157,6 +157,23 @@ class IndexedDBStorage implements StorageAdapter, Auth0Cache {
}
}
async clearAll(): Promise<void> {
await this.initPromise;
if (!this.db) {
this.memoryCache.clear();
return;
}
const tx = this.db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).clear();
await new Promise<void>((resolve, reject) => {
tx.oncomplete = () => {
this.memoryCache.clear();
resolve();
};
tx.onerror = () => reject(tx.error);
});
}
key(index: number): string | null {
const keys = Array.from(this.memoryCache.keys());
return keys[index] || null;

View File

@@ -6,9 +6,10 @@ import { FeaturesGrid } from './HomePage/FeaturesGrid';
import { motion } from 'framer-motion';
export const HomePage = () => {
const { loginWithRedirect, isAuthenticated } = useAuth0();
const { loginWithRedirect, isAuthenticated, logout } = useAuth0();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [sessionCleared, setSessionCleared] = useState(false);
const navigate = useNavigate();
useEffect(() => {
@@ -41,6 +42,22 @@ export const HomePage = () => {
navigate('/signup');
};
const handleClearSession = async () => {
try {
const { indexedDBStorage } = await import('../core/utils/indexeddb-storage');
await indexedDBStorage.clearAll();
Object.keys(localStorage).forEach(key => {
if (key.startsWith('@@auth0')) localStorage.removeItem(key);
});
logout({ openUrl: false });
setSessionCleared(true);
setTimeout(() => setSessionCleared(false), 3000);
} catch (error) {
console.error('[HomePage] Failed to clear session:', error);
window.location.reload();
}
};
return (
<div className="min-h-screen bg-nero text-avus">
{/* Navigation Bar */}
@@ -84,6 +101,12 @@ export const HomePage = () => {
>
Login
</button>
<button
onClick={handleClearSession}
className="text-white/40 hover:text-white/70 text-xs transition-colors min-h-[44px] min-w-[44px] flex items-center"
>
{sessionCleared ? 'Session cleared' : 'Trouble logging in?'}
</button>
</div>
{/* Mobile Menu Button */}
@@ -149,6 +172,12 @@ export const HomePage = () => {
>
Login
</button>
<button
onClick={handleClearSession}
className="w-full text-white/40 hover:text-white/70 text-xs py-2 min-h-[44px] transition-colors"
>
{sessionCleared ? 'Session cleared' : 'Trouble logging in?'}
</button>
</motion.div>
)}
</div>