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:
Dario Ghunney Ware
2025-11-06 15:42:22 +00:00
committed by GitHub
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
18 changed files with 760 additions and 197 deletions
+48 -17
View File
@@ -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;
}