diff --git a/app/core/src/main/resources/application.properties b/app/core/src/main/resources/application.properties index 1f4e831df..0a215ef23 100644 --- a/app/core/src/main/resources/application.properties +++ b/app/core/src/main/resources/application.properties @@ -60,4 +60,4 @@ spring.main.allow-bean-definition-overriding=true java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf} # V2 features -v2=false +v2=true diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index e53a0e8a5..889391e58 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -139,11 +139,15 @@ public class SecurityConfiguration { .exceptionHandling( exceptionHandling -> exceptionHandling.authenticationEntryPoint( - jwtAuthenticationEntryPoint)); + jwtAuthenticationEntryPoint)) + .addFilterAfter( + userAuthenticationFilter, + JwtAuthenticationFilter.class); // Run AFTER JWT filter + } else { + http.addFilterBefore( + userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } - http.addFilterBefore( - userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterAfter(rateLimitingFilter(), UserAuthenticationFilter.class) + http.addFilterAfter(rateLimitingFilter(), UserAuthenticationFilter.class) .addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); if (!securityProperties.getCsrfDisabled()) { @@ -245,6 +249,7 @@ public class SecurityConfiguration { : uri; return trimmedUri.startsWith("/login") || trimmedUri.startsWith("/oauth") + || trimmedUri.startsWith("/oauth2") || trimmedUri.startsWith("/saml2") || trimmedUri.endsWith(".svg") || trimmedUri.startsWith("/register") @@ -293,33 +298,39 @@ public class SecurityConfiguration { // Handle OAUTH2 Logins if (securityProperties.isOauth2Active()) { http.oauth2Login( - oauth2 -> - oauth2.loginPage("/oauth2") - /* - This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database. - If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser' - is set as true, else login fails with an error message advising the same. - */ - .successHandler( - new CustomOAuth2AuthenticationSuccessHandler( - loginAttemptService, - securityProperties.getOauth2(), - userService, - jwtService)) - .failureHandler( - new CustomOAuth2AuthenticationFailureHandler()) - // Add existing Authorities from the database - .userInfoEndpoint( - userInfoEndpoint -> - userInfoEndpoint - .oidcUserService( - new CustomOAuth2UserService( - securityProperties, - userService, - loginAttemptService)) - .userAuthoritiesMapper( - oAuth2userAuthoritiesMapper)) - .permitAll()); + oauth2 -> { + // v2: Don't set loginPage, let default OAuth2 flow handle it + // v1: Use /oauth2 as login page for Thymeleaf templates + if (!v2Enabled) { + oauth2.loginPage("/oauth2"); + } + + oauth2 + /* + This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database. + If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser' + is set as true, else login fails with an error message advising the same. + */ + .successHandler( + new CustomOAuth2AuthenticationSuccessHandler( + loginAttemptService, + securityProperties.getOauth2(), + userService, + jwtService)) + .failureHandler(new CustomOAuth2AuthenticationFailureHandler()) + // Add existing Authorities from the database + .userInfoEndpoint( + userInfoEndpoint -> + userInfoEndpoint + .oidcUserService( + new CustomOAuth2UserService( + securityProperties, + userService, + loginAttemptService)) + .userAuthoritiesMapper( + oAuth2userAuthoritiesMapper)) + .permitAll(); + }); } // Handle SAML if (securityProperties.isSaml2Active() && runningProOrHigher) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java index d1e57cc58..69d37ba28 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java @@ -75,28 +75,49 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { String jwtToken = jwtService.extractToken(request); if (jwtToken == null) { - // Allow auth endpoints to pass through without JWT + // Allow specific auth endpoints to pass through without JWT String requestURI = request.getRequestURI(); String contextPath = request.getContextPath(); - // Skip redirect for auth endpoints (they'll handle their own auth checks) - if (!requestURI.startsWith(contextPath + "/login") - && !requestURI.startsWith(contextPath + "/api/v1/auth")) { - response.sendRedirect("/login"); - return; - } + // Public auth endpoints that don't require JWT + boolean isPublicAuthEndpoint = + requestURI.startsWith(contextPath + "/login") + || requestURI.startsWith(contextPath + "/signup") + || requestURI.startsWith(contextPath + "/auth/") + || requestURI.startsWith(contextPath + "/oauth2") + || requestURI.startsWith(contextPath + "/api/v1/auth/login") + || requestURI.startsWith(contextPath + "/api/v1/auth/register") + || requestURI.startsWith(contextPath + "/api/v1/auth/refresh"); - // For auth endpoints without JWT, continue to the endpoint - // (it will return 401 if needed) - if (requestURI.startsWith(contextPath + "/api/v1/auth")) { + if (!isPublicAuthEndpoint) { + // For API requests, return 401 JSON + String acceptHeader = request.getHeader("Accept"); + if (requestURI.startsWith(contextPath + "/api/") + || (acceptHeader != null + && acceptHeader.contains("application/json"))) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.getWriter().write("{\"error\":\"Authentication required\"}"); + return; + } + + // For HTML requests (SPA routes), let React Router handle it (serve + // index.html) filterChain.doFilter(request, response); return; } + + // For public auth endpoints without JWT, continue to the endpoint + filterChain.doFilter(request, response); + return; } try { + log.debug("Validating JWT token"); jwtService.validateToken(jwtToken); + log.debug("JWT token validated successfully"); } catch (AuthenticationFailureException e) { + log.warn("JWT validation failed: {}", e.getMessage()); jwtService.clearToken(response); handleAuthenticationFailure(request, response, e); return; @@ -104,9 +125,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { Map claims = jwtService.extractClaims(jwtToken); String tokenUsername = claims.get("sub").toString(); + log.debug("JWT token username: {}", tokenUsername); try { authenticate(request, claims); + log.debug("Authentication successful for user: {}", tokenUsername); } catch (SQLException | UnsupportedProviderException e) { log.error("Error processing user authentication for user: {}", tokenUsername, e); handleAuthenticationFailure( diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java index 5b2b3b281..8bf8bdd4a 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java @@ -239,6 +239,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { contextPath + "/api/v1/auth/login", contextPath + "/api/v1/auth/register", contextPath + "/api/v1/auth/refresh", + contextPath + "/api/v1/auth/me", contextPath + "/site.webmanifest" }; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java index 4e7ed9d9e..99daa6c44 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java @@ -72,12 +72,6 @@ public class CustomOAuth2AuthenticationSuccessHandler throw new LockedException( "Your account has been locked due to too many failed login attempts."); } - if (jwtService.isJwtEnabled()) { - String jwt = - jwtService.generateToken( - authentication, Map.of("authType", AuthenticationType.OAUTH2)); - jwtService.addToken(response, jwt); - } if (userService.isUserDisabled(username)) { getRedirectStrategy() .sendRedirect(request, response, "/logout?userIsDisabled=true"); @@ -102,7 +96,19 @@ public class CustomOAuth2AuthenticationSuccessHandler userService.processSSOPostLogin( username, oauth2Properties.getAutoCreateUser(), OAUTH2); } - response.sendRedirect(contextPath + "/"); + + // Generate JWT if v2 is enabled + if (jwtService.isJwtEnabled()) { + String jwt = + jwtService.generateToken( + authentication, Map.of("authType", AuthenticationType.OAUTH2)); + jwtService.addToken(response, jwt); + // Redirect to auth callback for v2 (React will handle final routing) + response.sendRedirect(contextPath + "/auth/callback"); + } else { + // v1: redirect directly to home + response.sendRedirect(contextPath + "/"); + } } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { response.sendRedirect(contextPath + "/logout?invalidUsername=true"); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java index 3255cbc15..1575f8643 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticationSuccessHandler.java @@ -122,7 +122,14 @@ public class CustomSaml2AuthenticationSuccessHandler log.debug("Successfully processed authentication for user: {}", username); generateJwt(response, authentication); - response.sendRedirect(contextPath + "/"); + + // Redirect to auth callback for v2 (React will handle final routing) + if (jwtService.isJwtEnabled()) { + response.sendRedirect(contextPath + "/auth/callback"); + } else { + // v1: redirect directly to home + response.sendRedirect(contextPath + "/"); + } } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { log.debug( "Invalid username detected for user: {}, redirecting to logout", diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java index 06dd81c95..51e9e75ad 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java @@ -44,11 +44,11 @@ import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrin public class JwtService implements JwtServiceInterface { private static final String JWT_COOKIE_NAME = "stirling_jwt"; - private static final String ISSUER = "Stirling PDF"; + private static final String ISSUER = "https://stirling.com"; private static final long EXPIRATION = 3600000; - @Value("${stirling.security.jwt.secureCookie:true}") - private boolean secureCookie; + @Value("${stirling.security.jwt.secureCookie:false}") + private boolean secureCookie = false; // Hardcoded to false for HTTP development private final KeyPersistenceServiceInterface keyPersistenceService; private final boolean v2Enabled; @@ -59,6 +59,7 @@ public class JwtService implements JwtServiceInterface { KeyPersistenceServiceInterface keyPersistenceService) { this.v2Enabled = v2Enabled; this.keyPersistenceService = keyPersistenceService; + log.info("JwtService initialized with secureCookie={}", secureCookie); } @Override @@ -260,24 +261,37 @@ public class JwtService implements JwtServiceInterface { @Override public String extractToken(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); + // First, try to extract from Authorization header (Bearer token) + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); // Remove "Bearer " prefix + log.debug("JWT token extracted from Authorization header"); + return token; + } + // Fall back to cookie-based authentication + Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (JWT_COOKIE_NAME.equals(cookie.getName())) { + log.debug("JWT token extracted from cookie"); return cookie.getValue(); } } } + log.debug("No JWT token found in Authorization header or cookies"); return null; } @Override public void addToken(HttpServletResponse response, String token) { + log.info("Setting JWT cookie with secureCookie={}", secureCookie); ResponseCookie cookie = ResponseCookie.from(JWT_COOKIE_NAME, Newlines.stripAll(token)) - .httpOnly(true) + .httpOnly( + false) // Set to false for V2 to allow JavaScript to read token for + // Authorization header .secure(secureCookie) .sameSite("Lax") // Changed from Strict to Lax for cross-port dev // compatibility @@ -287,23 +301,28 @@ public class JwtService implements JwtServiceInterface { // compatibility .build(); - response.addHeader("Set-Cookie", cookie.toString()); + String cookieString = cookie.toString(); + log.info("Set-Cookie header: {}", cookieString); + response.addHeader("Set-Cookie", cookieString); } @Override public void clearToken(HttpServletResponse response) { + log.info("Clearing JWT cookie"); ResponseCookie cookie = ResponseCookie.from(JWT_COOKIE_NAME, "") - .httpOnly(true) + .httpOnly(false) // Must match addToken settings .secure(secureCookie) - .sameSite("None") + .sameSite("Lax") // Must match addToken settings .maxAge(0) .path("/") .domain("localhost") // Set domain to localhost for cross-port dev // compatibility .build(); - response.addHeader("Set-Cookie", cookie.toString()); + String cookieString = cookie.toString(); + log.info("Clear-Cookie header: {}", cookieString); + response.addHeader("Set-Cookie", cookieString); } @Override diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5b0d061da..f456aa662 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,7 +19,6 @@ import OnboardingTour from "./components/onboarding/OnboardingTour"; import { AuthProvider } from "./auth/UseSession"; import Landing from "./routes/Landing"; import Login from "./routes/Login"; -import Signup from "./routes/Signup"; import AuthCallback from "./routes/AuthCallback"; // Import global styles @@ -59,7 +58,6 @@ export default function App() { {/* Auth routes - no FileContext or other providers needed */} } /> - } /> } /> {/* Main app routes - wrapped with all providers */} diff --git a/frontend/src/auth/springAuthClient.ts b/frontend/src/auth/springAuthClient.ts index 6b9d4623e..2c55a0c71 100644 --- a/frontend/src/auth/springAuthClient.ts +++ b/frontend/src/auth/springAuthClient.ts @@ -62,6 +62,22 @@ class SpringAuthClient { this.startSessionMonitoring(); } + /** + * Helper to get JWT token from cookie and add to fetch headers + */ + private getAuthHeaders(): HeadersInit { + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const [name, value] = cookie.trim().split('='); + if (name === 'stirling_jwt') { + return { + 'Authorization': `Bearer ${value}`, + }; + } + } + return {}; + } + /** * Get current session * Note: JWT is stored in HttpOnly cookie, so we can't read it directly @@ -69,13 +85,18 @@ class SpringAuthClient { */ async getSession(): Promise<{ data: { session: Session | null }; error: AuthError | null }> { try { - // Verify with backend + // Verify with backend - add Authorization header from cookie + const authHeaders = this.getAuthHeaders(); const response = await fetch('/api/v1/auth/me', { credentials: 'include', // Include cookies + headers: { + ...authHeaders, + }, }); if (!response.ok) { // Not authenticated + console.debug('[SpringAuth] getSession: Not authenticated (status:', response.status, ')'); return { data: { session: null }, error: null }; } @@ -89,6 +110,7 @@ class SpringAuthClient { expires_at: Date.now() + 3600 * 1000, }; + console.debug('[SpringAuth] getSession: Session retrieved successfully'); return { data: { session }, error: null }; } catch (error) { console.error('[SpringAuth] getSession error:', error); @@ -190,9 +212,11 @@ class SpringAuthClient { options?: { redirectTo?: string; queryParams?: Record }; }): Promise<{ error: AuthError | null }> { try { - // Redirect to Spring OAuth2 endpoint + // Redirect to Spring OAuth2 endpoint (Vite will proxy to backend) const redirectUrl = `/oauth2/authorization/${params.provider}`; - window.location.href = redirectUrl; + console.log('[SpringAuth] Redirecting to OAuth:', redirectUrl); + // Use window.location.assign for full page navigation + window.location.assign(redirectUrl); return { error: null }; } catch (error) { return { diff --git a/frontend/src/components/shared/config/configSections/Overview.tsx b/frontend/src/components/shared/config/configSections/Overview.tsx index d3f250f49..b3a192cd0 100644 --- a/frontend/src/components/shared/config/configSections/Overview.tsx +++ b/frontend/src/components/shared/config/configSections/Overview.tsx @@ -1,9 +1,13 @@ import React from 'react'; -import { Stack, Text, Code, Group, Badge, Alert, Loader } from '@mantine/core'; +import { Stack, Text, Code, Group, Badge, Alert, Loader, Button } from '@mantine/core'; import { useAppConfig } from '../../../../hooks/useAppConfig'; +import { useAuth } from '../../../../auth/UseSession'; +import { useNavigate } from 'react-router-dom'; const Overview: React.FC = () => { const { config, loading, error } = useAppConfig(); + const { signOut, user } = useAuth(); + const navigate = useNavigate(); const renderConfigSection = (title: string, data: any) => { if (!data || typeof data !== 'object') return null; @@ -54,6 +58,15 @@ const Overview: React.FC = () => { SSOAutoLogin: config.SSOAutoLogin, } : null; + const handleLogout = async () => { + try { + await signOut(); + navigate('/login'); + } catch (error) { + console.error('Logout error:', error); + } + }; + if (loading) { return ( @@ -74,10 +87,24 @@ const Overview: React.FC = () => { return (
- Application Configuration - - Current application settings and configuration details. - +
+
+ Application Configuration + + Current application settings and configuration details. + + {user?.email && ( + + Signed in as: {user.email} + + )} +
+ {user && ( + + )} +
{config && ( diff --git a/frontend/src/routes/Signup.tsx b/frontend/src/routes/Signup.tsx deleted file mode 100644 index e54b81a27..000000000 --- a/frontend/src/routes/Signup.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { springAuth } from '../auth/springAuthClient' -import { useAuth } from '../auth/UseSession' -import { useTranslation } from 'react-i18next' -import { useDocumentMeta } from '../hooks/useDocumentMeta' -import { BASE_PATH } from '../constants/app' -import AuthLayout from './authShared/AuthLayout' - -// Import signup components -import LoginHeader from './login/LoginHeader' -import ErrorMessage from './login/ErrorMessage' -import OAuthButtons from './login/OAuthButtons' -import DividerWithText from '../components/shared/DividerWithText' -import NavigationLink from './login/NavigationLink' -import SignupForm from './signup/SignupForm' -import { useSignupFormValidation, SignupFieldErrors } from './signup/SignupFormValidation' - -export default function Signup() { - const navigate = useNavigate() - const { session, loading } = useAuth() - const { t } = useTranslation() - const [isSigningUp, setIsSigningUp] = useState(false) - const [error, setError] = useState(null) - const [name, setName] = useState('') - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [agree, setAgree] = useState(true) - const [fieldErrors, setFieldErrors] = useState({}) - - const baseUrl = window.location.origin + BASE_PATH; - - // Set document meta - useDocumentMeta({ - title: `${t('signup.title', 'Create an account')} - Stirling PDF`, - description: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'), - ogTitle: `${t('signup.title', 'Create an account')} - Stirling PDF`, - ogDescription: t('app.description', 'The Free Adobe Acrobat alternative (10M+ Downloads)'), - ogImage: `${baseUrl}/og_images/home.png`, - ogUrl: `${window.location.origin}${window.location.pathname}` - }) - - const { validateSignupForm } = useSignupFormValidation() - - const handleSignUp = async () => { - const validation = validateSignupForm(email, password, confirmPassword, name) - if (!validation.isValid) { - setError(validation.error) - setFieldErrors(validation.fieldErrors || {}) - return - } - - try { - setIsSigningUp(true) - setError(null) - setFieldErrors({}) - - console.log('[Signup] Creating account for:', email) - - const { user, error } = await springAuth.signUp({ - email: email.trim(), - password: password, - options: { - data: { full_name: name } - } - }) - - if (error) { - console.error('[Signup] Sign up error:', error) - setError(error.message) - } else if (user) { - console.log('[Signup] Account created successfully') - // Show success message - alert(t('signup.accountCreatedSuccessfully') || 'Account created successfully! Please log in.') - // Redirect to login - setTimeout(() => navigate('/login'), 1000) - } - } catch (err) { - console.error('[Signup] Unexpected error:', err) - setError(err instanceof Error ? err.message : (t('signup.unexpectedError', { message: 'Unknown error' }) || 'An unexpected error occurred')) - } finally { - setIsSigningUp(false) - } - } - - const handleProviderSignIn = async (provider: 'github' | 'google' | 'apple' | 'azure') => { - try { - setIsSigningUp(true) - setError(null) - - console.log(`[Signup] Signing up with ${provider}`) - - const { error } = await springAuth.signInWithOAuth({ - provider, - options: { redirectTo: `${BASE_PATH}/auth/callback` } - }) - - if (error) { - setError(error.message) - } - } catch (err) { - setError(err instanceof Error ? err.message : (t('signup.unexpectedError', { message: 'Unknown error' }) || 'An unexpected error occurred')) - } finally { - setIsSigningUp(false) - } - } - - return ( - - - - - - - -
- -
- -
- -
- -
- navigate('/login')} - text={t('signup.alreadyHaveAccount') || 'Already have an account? Sign in'} - isDisabled={isSigningUp} - /> -
-
- ) -} diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts index 536c47448..a194a64c5 100644 --- a/frontend/src/services/apiClient.ts +++ b/frontend/src/services/apiClient.ts @@ -8,6 +8,37 @@ const apiClient = axios.create({ responseType: 'json', }); +// Helper function to get JWT token from cookies +function getJwtTokenFromCookie(): string | null { + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const [name, value] = cookie.trim().split('='); + if (name === 'stirling_jwt') { + return value; + } + } + return null; +} + +// ---------- Install request interceptor to add JWT token ---------- +apiClient.interceptors.request.use( + (config) => { + // Get JWT token from cookie + const jwtToken = getJwtTokenFromCookie(); + + // If token exists and Authorization header is not already set, add it + if (jwtToken && !config.headers.Authorization) { + config.headers.Authorization = `Bearer ${jwtToken}`; + console.debug('[API Client] Added JWT token from cookie to Authorization header'); + } + + return config; + }, + (error) => { + return Promise.reject(error); + } +); + // ---------- Install error interceptor ---------- apiClient.interceptors.response.use( (response) => response,