mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Login Refresh Fix (#4779)
Main Issues Fixed: 1. Tools Disabled on Initial Login (Required Page Refresh) Problem: After successful login, all PDF tools appeared grayed out/disabled until the user refreshed the page. Root Cause: Race condition where tools checked endpoint availability before JWT was stored in localStorage. Fix: - Implemented optimistic defaults in useEndpointConfig - assumes endpoints are enabled when no JWT exists - Added JWT availability event system (jwt-available event) to notify components when authentication is ready - Tools now remain enabled during auth initialization instead of defaulting to disabled 2. Session Lost on Page Refresh (Immediate Logout) Problem: Users were immediately logged out when refreshing the page, losing their authenticated session. Root Causes: - Spring Security form login was redirecting API calls to /login with 302 responses instead of returning JSON - /api/v1/auth/me endpoint was incorrectly in the permitAll list - JWT filter wasn't allowing /api/v1/config endpoints without authentication Fixes: - Backend: Disabled form login in v2/JWT mode by adding && !v2Enabled condition to form login configuration - Backend: Removed /api/v1/auth/me from permitAll list - it now requires authentication - Backend: Added /api/v1/config to public endpoints in JWT filter - Backend: Configured proper exception handling for API endpoints to return JSON (401) instead of HTML redirects (302) 3. Multiple Duplicate API Calls Problem: After login, /app-config was called 5+ times, /endpoints-enabled and /me called multiple times, causing unnecessary network traffic. Root Cause: Multiple React components each had their own instance of useAppConfig and useEndpointConfig hooks, each fetching data independently. Fix: - Frontend: Created singleton AppConfigContext provider to ensure only one global config fetch - Frontend: Added global caching to useEndpointConfig with module-level cache variables - Frontend: Implemented fetch deduplication with fetchCount tracking and globalFetchedSets - Result: Reduced API calls from 5+ to 1-2 per endpoint (2 in dev due to React StrictMode) Additional Improvements: CORS Configuration - Added flexible CORS configuration matching SaaS pattern - Explicitly allows localhost development ports (3000, 5173, 5174, etc.) - No hardcoded URLs in application.properties Security Handlers Integration - Added IP-based account locking without dependency on form login - Preserved audit logging with @Audited annotations Key Code Changes: Backend Files: - SecurityConfiguration.java - Disabled form login for v2, added CORS config - JwtAuthenticationFilter.java - Added /api/v1/config to public endpoints - JwtAuthenticationEntryPoint.java - Returns JSON for API requests Frontend Files: - AppConfigContext.tsx - New singleton context for app configuration - useEndpointConfig.ts - Added global caching and deduplication - UseSession.tsx - Removed redundant config checking - Various hooks - Updated to use context providers instead of direct fetching --------- Signed-off-by: dependabot[bot] <[email protected]> Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ludy <[email protected]> Co-authored-by: EthanHealy01 <[email protected]> Co-authored-by: Ethan <[email protected]> Co-authored-by: Anthony Stirling <[email protected]> Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
co-authored by
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Ludy
EthanHealy01
Ethan
Anthony Stirling
stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
ConnorYoh
Connor Yoh
parent
9440e99227
commit
f5c67a3239
@@ -1,5 +1,5 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { useRequestHeaders } from '@app/hooks/useRequestHeaders';
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
export interface AppConfig {
|
||||
baseUrl?: string;
|
||||
@@ -36,52 +36,84 @@ interface AppConfigContextValue {
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Create context
|
||||
const AppConfigContext = createContext<AppConfigContextValue | undefined>(undefined);
|
||||
const AppConfigContext = createContext<AppConfigContextValue | undefined>({
|
||||
config: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
refetch: async () => {},
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider component that fetches and provides app configuration
|
||||
* Should be placed at the top level of the app, before any components that need config
|
||||
*/
|
||||
export const AppConfigProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [config, setConfig] = useState<AppConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const headers = useRequestHeaders();
|
||||
const [fetchCount, setFetchCount] = useState(0);
|
||||
|
||||
const fetchConfig = async (force = false) => {
|
||||
// Prevent duplicate fetches unless forced
|
||||
if (!force && fetchCount > 0) {
|
||||
console.debug('[AppConfig] Already fetched, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch('/api/v1/config/app-config', {
|
||||
headers,
|
||||
});
|
||||
// apiClient automatically adds JWT header if available via interceptors
|
||||
const response = await apiClient.get<AppConfig>('/api/v1/config/app-config');
|
||||
const data = response.data;
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`);
|
||||
console.debug('[AppConfig] Config fetched successfully:', data);
|
||||
setConfig(data);
|
||||
setFetchCount(prev => prev + 1);
|
||||
} catch (err: any) {
|
||||
// On 401 (not authenticated), use default config with login enabled
|
||||
// This allows the app to work even without authentication
|
||||
if (err.response?.status === 401) {
|
||||
console.debug('[AppConfig] 401 error - using default config (login enabled)');
|
||||
setConfig({ enableLogin: true });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const data: AppConfig = await response.json();
|
||||
setConfig(data);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
setError(errorMessage);
|
||||
console.error('[AppConfig] Failed to fetch app config:', err);
|
||||
// On error, assume login is enabled (safe default)
|
||||
setConfig({ enableLogin: true });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Always try to fetch config to check if login is disabled
|
||||
// The endpoint should be public and return proper JSON
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
// Listen for JWT availability (triggered on login/signup)
|
||||
useEffect(() => {
|
||||
const handleJwtAvailable = () => {
|
||||
console.debug('[AppConfig] JWT available event - refetching config');
|
||||
// Force refetch with JWT
|
||||
fetchConfig(true);
|
||||
};
|
||||
|
||||
window.addEventListener('jwt-available', handleJwtAvailable);
|
||||
return () => window.removeEventListener('jwt-available', handleJwtAvailable);
|
||||
}, []);
|
||||
|
||||
const value: AppConfigContextValue = {
|
||||
config,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchConfig,
|
||||
refetch: () => fetchConfig(true),
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -104,4 +136,3 @@ export function useAppConfig(): AppConfigContextValue {
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRequestHeaders } from '@app/hooks/useRequestHeaders';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
// Track globally fetched endpoint sets to prevent duplicate fetches across components
|
||||
const globalFetchedSets = new Set<string>();
|
||||
const globalEndpointCache: Record<string, boolean> = {};
|
||||
|
||||
/**
|
||||
* Hook to check if a specific endpoint is enabled
|
||||
* This wraps the context for single endpoint checks
|
||||
*/
|
||||
export function useEndpointEnabled(endpoint: string): {
|
||||
enabled: boolean | null;
|
||||
@@ -13,7 +18,6 @@ export function useEndpointEnabled(endpoint: string): {
|
||||
const [enabled, setEnabled] = useState<boolean | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const headers = useRequestHeaders();
|
||||
|
||||
const fetchEndpointStatus = async () => {
|
||||
if (!endpoint) {
|
||||
@@ -26,15 +30,8 @@ export function useEndpointEnabled(endpoint: string): {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to check endpoint: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const isEnabled: boolean = await response.json();
|
||||
const response = await apiClient.get<boolean>(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`);
|
||||
const isEnabled = response.data;
|
||||
setEnabled(isEnabled);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
@@ -69,43 +66,101 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
||||
const [endpointStatus, setEndpointStatus] = useState<Record<string, boolean>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const headers = useRequestHeaders();
|
||||
|
||||
const fetchAllEndpointStatuses = async () => {
|
||||
const fetchAllEndpointStatuses = async (force = false) => {
|
||||
const endpointsKey = [...endpoints].sort().join(',');
|
||||
|
||||
// Skip if we already fetched these exact endpoints globally
|
||||
if (!force && globalFetchedSets.has(endpointsKey)) {
|
||||
console.debug('[useEndpointConfig] Already fetched these endpoints globally, using cache');
|
||||
const cachedStatus = endpoints.reduce((acc, endpoint) => {
|
||||
if (endpoint in globalEndpointCache) {
|
||||
acc[endpoint] = globalEndpointCache[endpoint];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
setEndpointStatus(cachedStatus);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!endpoints || endpoints.length === 0) {
|
||||
setEndpointStatus({});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if JWT exists - if not, optimistically enable all endpoints
|
||||
const hasJwt = !!localStorage.getItem('stirling_jwt');
|
||||
if (!hasJwt) {
|
||||
console.debug('[useEndpointConfig] No JWT found - optimistically enabling all endpoints');
|
||||
const optimisticStatus = endpoints.reduce((acc, endpoint) => {
|
||||
acc[endpoint] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
setEndpointStatus(optimisticStatus);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Use batch API for efficiency
|
||||
const endpointsParam = endpoints.join(',');
|
||||
|
||||
const response = await fetch(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to check endpoints: ${response.status} ${response.statusText}`);
|
||||
// Check which endpoints we haven't fetched yet
|
||||
const newEndpoints = endpoints.filter(ep => !(ep in globalEndpointCache));
|
||||
if (newEndpoints.length === 0) {
|
||||
console.debug('[useEndpointConfig] All endpoints already in global cache');
|
||||
const cachedStatus = endpoints.reduce((acc, endpoint) => {
|
||||
acc[endpoint] = globalEndpointCache[endpoint];
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
setEndpointStatus(cachedStatus);
|
||||
globalFetchedSets.add(endpointsKey);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const statusMap: Record<string, boolean> = await response.json();
|
||||
setEndpointStatus(statusMap);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
setError(errorMessage);
|
||||
console.error('Failed to check multiple endpoints:', err);
|
||||
// Use batch API for efficiency - only fetch new endpoints
|
||||
const endpointsParam = newEndpoints.join(',');
|
||||
|
||||
// Fallback: assume all endpoints are disabled on error
|
||||
const fallbackStatus = endpoints.reduce((acc, endpoint) => {
|
||||
acc[endpoint] = false;
|
||||
const response = await apiClient.get<Record<string, boolean>>(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`);
|
||||
const statusMap = response.data;
|
||||
|
||||
// Update global cache with new results
|
||||
Object.assign(globalEndpointCache, statusMap);
|
||||
|
||||
// Get all requested endpoints from cache (including previously cached ones)
|
||||
const fullStatus = endpoints.reduce((acc, endpoint) => {
|
||||
acc[endpoint] = globalEndpointCache[endpoint] ?? true; // Default to true if not in cache
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
setEndpointStatus(fallbackStatus);
|
||||
|
||||
setEndpointStatus(fullStatus);
|
||||
globalFetchedSets.add(endpointsKey);
|
||||
} catch (err: any) {
|
||||
// On 401 (auth error), use optimistic fallback instead of disabling
|
||||
if (err.response?.status === 401) {
|
||||
console.warn('[useEndpointConfig] 401 error - using optimistic fallback');
|
||||
const optimisticStatus = endpoints.reduce((acc, endpoint) => {
|
||||
acc[endpoint] = true;
|
||||
globalEndpointCache[endpoint] = true; // Cache the optimistic value
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
setEndpointStatus(optimisticStatus);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
setError(errorMessage);
|
||||
console.error('[EndpointConfig] Failed to check multiple endpoints:', err);
|
||||
|
||||
// Fallback: assume all endpoints are enabled on error (optimistic)
|
||||
const optimisticStatus = endpoints.reduce((acc, endpoint) => {
|
||||
acc[endpoint] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
setEndpointStatus(optimisticStatus);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -115,10 +170,24 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
||||
fetchAllEndpointStatuses();
|
||||
}, [endpoints.join(',')]); // Re-run when endpoints array changes
|
||||
|
||||
// Listen for JWT availability (triggered on login/signup)
|
||||
useEffect(() => {
|
||||
const handleJwtAvailable = () => {
|
||||
console.debug('[useEndpointConfig] JWT available event - clearing cache for refetch with auth');
|
||||
// Clear the global cache to allow refetch with JWT
|
||||
globalFetchedSets.clear();
|
||||
Object.keys(globalEndpointCache).forEach(key => delete globalEndpointCache[key]);
|
||||
fetchAllEndpointStatuses(true);
|
||||
};
|
||||
|
||||
window.addEventListener('jwt-available', handleJwtAvailable);
|
||||
return () => window.removeEventListener('jwt-available', handleJwtAvailable);
|
||||
}, [endpoints.join(',')]);
|
||||
|
||||
return {
|
||||
endpointStatus,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchAllEndpointStatuses,
|
||||
refetch: () => fetchAllEndpointStatuses(true),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,10 +24,18 @@ export const useToolManagement = (): ToolManagementResult => {
|
||||
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
||||
|
||||
const isToolAvailable = useCallback((toolKey: string): boolean => {
|
||||
// Keep tools enabled during loading (optimistic UX)
|
||||
if (endpointsLoading) return true;
|
||||
|
||||
const tool = baseRegistry[toolKey as ToolId];
|
||||
const endpoints = tool?.endpoints || [];
|
||||
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
|
||||
|
||||
// Tools without endpoints are always available
|
||||
if (endpoints.length === 0) return true;
|
||||
|
||||
// Check if at least one endpoint is enabled
|
||||
// If endpoint is not in status map, assume enabled (optimistic fallback)
|
||||
return endpoints.some((endpoint: string) => endpointStatus[endpoint] !== false);
|
||||
}, [endpointsLoading, endpointStatus, baseRegistry]);
|
||||
|
||||
const toolRegistry: Partial<ToolRegistry> = useMemo(() => {
|
||||
|
||||
@@ -95,23 +95,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
try {
|
||||
console.debug('[Auth] Initializing auth...');
|
||||
|
||||
// First check if login is enabled
|
||||
const configResponse = await fetch('/api/v1/config/app-config');
|
||||
if (configResponse.ok) {
|
||||
const config = await configResponse.json();
|
||||
|
||||
// If login is disabled, skip authentication entirely
|
||||
if (config.enableLogin === false) {
|
||||
console.debug('[Auth] Login disabled - skipping authentication');
|
||||
if (mounted) {
|
||||
setSession(null);
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Login is enabled, proceed with normal auth check
|
||||
// Skip config check entirely - let the app handle login state
|
||||
// The config will be fetched by useAppConfig when needed
|
||||
const { data, error } = await springAuth.getSession();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
|
||||
/**
|
||||
* Spring Auth Client
|
||||
*
|
||||
@@ -7,6 +9,37 @@
|
||||
* - No email confirmation flow (auto-confirmed on registration)
|
||||
*/
|
||||
|
||||
const OAUTH_REDIRECT_COOKIE = 'stirling_redirect_path';
|
||||
const OAUTH_REDIRECT_COOKIE_MAX_AGE = 60 * 5; // 5 minutes
|
||||
const DEFAULT_REDIRECT_PATH = `${BASE_PATH || ''}/auth/callback`;
|
||||
|
||||
function normalizeRedirectPath(target?: string): string {
|
||||
if (!target || typeof target !== 'string') {
|
||||
return DEFAULT_REDIRECT_PATH;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(target, window.location.origin);
|
||||
const path = parsed.pathname || '/';
|
||||
const query = parsed.search || '';
|
||||
return `${path}${query}`;
|
||||
} catch {
|
||||
const trimmed = target.trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_REDIRECT_PATH;
|
||||
}
|
||||
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
||||
}
|
||||
}
|
||||
|
||||
function persistRedirectPath(path: string): void {
|
||||
try {
|
||||
document.cookie = `${OAUTH_REDIRECT_COOKIE}=${encodeURIComponent(path)}; path=/; max-age=${OAUTH_REDIRECT_COOKIE_MAX_AGE}; SameSite=Lax`;
|
||||
} catch (error) {
|
||||
console.warn('[SpringAuth] Failed to persist OAuth redirect path', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Auth types
|
||||
export interface User {
|
||||
id: string;
|
||||
@@ -85,20 +118,44 @@ class SpringAuthClient {
|
||||
}
|
||||
|
||||
// Verify with backend
|
||||
console.debug('[SpringAuth] getSession: Verifying JWT with /api/v1/auth/me');
|
||||
const response = await fetch('/api/v1/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.debug('[SpringAuth] /me response status:', response.status);
|
||||
const contentType = response.headers.get('content-type');
|
||||
console.debug('[SpringAuth] /me content-type:', contentType);
|
||||
|
||||
if (!response.ok) {
|
||||
// Log the error response for debugging
|
||||
const errorBody = await response.text();
|
||||
console.error('[SpringAuth] getSession: /api/v1/auth/me failed', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorBody
|
||||
});
|
||||
|
||||
// Token invalid or expired - clear it
|
||||
localStorage.removeItem('stirling_jwt');
|
||||
console.debug('[SpringAuth] getSession: Not authenticated (status:', response.status, ')');
|
||||
return { data: { session: null }, error: null };
|
||||
console.warn('[SpringAuth] getSession: Cleared invalid JWT from localStorage');
|
||||
return { data: { session: null }, error: { message: `Auth failed: ${response.status}` } };
|
||||
}
|
||||
|
||||
// Check if response is JSON before parsing
|
||||
if (!contentType?.includes('application/json')) {
|
||||
const text = await response.text();
|
||||
console.error('[SpringAuth] /me returned non-JSON:', {
|
||||
contentType,
|
||||
bodyPreview: text.substring(0, 200)
|
||||
});
|
||||
throw new Error(`/api/v1/auth/me returned HTML instead of JSON`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.debug('[SpringAuth] /me response data:', data);
|
||||
|
||||
// Create session object
|
||||
const session: Session = {
|
||||
@@ -151,6 +208,9 @@ class SpringAuthClient {
|
||||
localStorage.setItem('stirling_jwt', token);
|
||||
console.log('[SpringAuth] JWT stored in localStorage');
|
||||
|
||||
// Dispatch custom event for other components to react to JWT availability
|
||||
window.dispatchEvent(new CustomEvent('jwt-available'));
|
||||
|
||||
const session: Session = {
|
||||
user: data.user,
|
||||
access_token: token,
|
||||
@@ -220,6 +280,9 @@ class SpringAuthClient {
|
||||
options?: { redirectTo?: string; queryParams?: Record<string, any> };
|
||||
}): Promise<{ error: AuthError | null }> {
|
||||
try {
|
||||
const redirectPath = normalizeRedirectPath(params.options?.redirectTo);
|
||||
persistRedirectPath(redirectPath);
|
||||
|
||||
// Redirect to Spring OAuth2 endpoint (Vite will proxy to backend)
|
||||
const redirectUrl = `/oauth2/authorization/${params.provider}`;
|
||||
console.log('[SpringAuth] Redirecting to OAuth:', redirectUrl);
|
||||
@@ -299,6 +362,9 @@ class SpringAuthClient {
|
||||
// Store new token
|
||||
localStorage.setItem('stirling_jwt', newToken);
|
||||
|
||||
// Dispatch custom event for other components to react to JWT availability
|
||||
window.dispatchEvent(new CustomEvent('jwt-available'));
|
||||
|
||||
// Get updated user info
|
||||
const userResponse = await fetch('/api/v1/auth/me', {
|
||||
headers: {
|
||||
|
||||
@@ -36,6 +36,9 @@ export default function AuthCallback() {
|
||||
localStorage.setItem('stirling_jwt', token);
|
||||
console.log('[AuthCallback] JWT stored in localStorage');
|
||||
|
||||
// Dispatch custom event for other components to react to JWT availability
|
||||
window.dispatchEvent(new CustomEvent('jwt-available'))
|
||||
|
||||
// Refresh session to load user info into state
|
||||
await refreshSession();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user