mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
Fixed login refresh, added logout
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<String, Object> 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(
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
<Routes>
|
||||
{/* Auth routes - no FileContext or other providers needed */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/signup" element={<Signup />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
|
||||
{/* Main app routes - wrapped with all providers */}
|
||||
|
||||
@@ -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<string, any> };
|
||||
}): 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 {
|
||||
|
||||
@@ -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 (
|
||||
<Stack align="center" py="md">
|
||||
@@ -74,10 +87,24 @@ const Overview: React.FC = () => {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">Application Configuration</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Current application settings and configuration details.
|
||||
</Text>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
|
||||
<div>
|
||||
<Text fw={600} size="lg">Application Configuration</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Current application settings and configuration details.
|
||||
</Text>
|
||||
{user?.email && (
|
||||
<Text size="xs" c="dimmed" mt="0.25rem">
|
||||
Signed in as: {user.email}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{user && (
|
||||
<Button color="red" variant="filled" onClick={handleLogout}>
|
||||
Log out
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config && (
|
||||
|
||||
@@ -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<string | null>(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<SignupFieldErrors>({})
|
||||
|
||||
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 (
|
||||
<AuthLayout>
|
||||
<LoginHeader title={t('signup.title') || 'Create an account'} subtitle={t('signup.subtitle')} />
|
||||
|
||||
<ErrorMessage error={error} />
|
||||
|
||||
<SignupForm
|
||||
name={name}
|
||||
email={email}
|
||||
password={password}
|
||||
confirmPassword={confirmPassword}
|
||||
agree={agree}
|
||||
setName={setName}
|
||||
setEmail={setEmail}
|
||||
setPassword={setPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
setAgree={setAgree}
|
||||
onSubmit={handleSignUp}
|
||||
isSubmitting={isSigningUp}
|
||||
fieldErrors={fieldErrors}
|
||||
/>
|
||||
|
||||
<div style={{ margin: '0.5rem 0' }}>
|
||||
<DividerWithText text={t('signup.or', "or")} respondsToDarkMode={false} opacity={0.4} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '0.5rem' }}>
|
||||
<OAuthButtons
|
||||
onProviderClick={handleProviderSignIn}
|
||||
isSubmitting={isSigningUp}
|
||||
layout="icons"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '0.5rem', textAlign: 'center' }}>
|
||||
<NavigationLink
|
||||
onClick={() => navigate('/login')}
|
||||
text={t('signup.alreadyHaveAccount') || 'Already have an account? Sign in'}
|
||||
isDisabled={isSigningUp}
|
||||
/>
|
||||
</div>
|
||||
</AuthLayout>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user