diff --git a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index dab11c697b..69aa60882f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.config; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -17,6 +18,17 @@ public class WebMvcConfig implements WebMvcConfigurer { registry.addInterceptor(endpointInterceptor); } + @Override + public void addCorsMappings(CorsRegistry registry) { + // Allow frontend dev server (Vite on localhost:5173) to access backend + registry.addMapping("/**") + .allowedOrigins("http://localhost:5173", "http://127.0.0.1:5173") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } + // @Override // public void addResourceHandlers(ResourceHandlerRegistry registry) { // // Handler for external static resources - DISABLED in backend-only mode 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 432d52d29d..e53a0e8a5a 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 @@ -156,6 +156,13 @@ public class SecurityConfiguration { csrf -> csrf.ignoringRequestMatchers( request -> { + String uri = request.getRequestURI(); + + // Ignore CSRF for auth endpoints + if (uri.startsWith("/api/v1/auth/")) { + return true; + } + String apiKey = request.getHeader("X-API-KEY"); // If there's no API key, don't ignore CSRF // (return false) @@ -254,9 +261,13 @@ public class SecurityConfiguration { || trimmedUri.startsWith("/favicon") || trimmedUri.startsWith( "/api/v1/info/status") - || trimmedUri.startsWith("/api/v1/auth/register") - || trimmedUri.startsWith("/api/v1/auth/login") - || trimmedUri.startsWith("/api/v1/auth/refresh") + || trimmedUri.startsWith( + "/api/v1/auth/register") + || trimmedUri.startsWith( + "/api/v1/auth/login") + || trimmedUri.startsWith( + "/api/v1/auth/refresh") + || trimmedUri.startsWith("/api/v1/auth/me") || trimmedUri.startsWith("/v1/api-docs") || uri.contains("/v1/api-docs"); }) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java index a89352cc3c..ee8dea7a24 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java @@ -26,11 +26,11 @@ import stirling.software.proprietary.security.service.JwtServiceInterface; import stirling.software.proprietary.security.service.UserService; /** - * REST API Controller for authentication operations. - * Replaces Supabase authentication with Spring Security + JWT. + * REST API Controller for authentication operations. Replaces Supabase authentication with Spring + * Security + JWT. * - * This controller provides endpoints matching the Supabase API surface - * to enable seamless frontend integration. + *
This controller provides endpoints matching the Supabase API surface to enable seamless
+ * frontend integration.
*/
@RestController
@RequestMapping("/api/v1/auth")
@@ -52,27 +52,26 @@ public class AuthController {
*/
@PostMapping("/login")
public ResponseEntity> login(
- @RequestBody LoginRequest request,
- HttpServletResponse response) {
+ @RequestBody LoginRequest request, HttpServletResponse response) {
try {
- log.debug("Login attempt for user: {}", request.getEmail());
+ log.debug("Login attempt for user: {}", request.email());
// Load user
- UserDetails userDetails = userDetailsService.loadUserByUsername(request.getEmail());
+ UserDetails userDetails = userDetailsService.loadUserByUsername(request.email());
User user = (User) userDetails;
// Validate password
- if (!userService.isPasswordCorrect(user, request.getPassword())) {
- log.warn("Invalid password for user: {}", request.getEmail());
+ if (!userService.isPasswordCorrect(user, request.password())) {
+ log.warn("Invalid password for user: {}", request.email());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
- .body(Map.of("error", "Invalid credentials"));
+ .body(Map.of("error", "Invalid credentials"));
}
// Check if user is enabled
if (!user.isEnabled()) {
- log.warn("Disabled user attempted login: {}", request.getEmail());
+ log.warn("Disabled user attempted login: {}", request.email());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
- .body(Map.of("error", "User account is disabled"));
+ .body(Map.of("error", "User account is disabled"));
}
// Generate JWT with claims
@@ -85,25 +84,22 @@ public class AuthController {
// Set JWT cookie (HttpOnly for security)
jwtService.addToken(response, token);
- log.info("Login successful for user: {}", request.getEmail());
+ log.info("Login successful for user: {}", request.email());
// Return user info (matches Supabase response structure)
- return ResponseEntity.ok(Map.of(
- "user", buildUserResponse(user),
- "session", Map.of(
- "access_token", token,
- "expires_in", 3600
- )
- ));
+ return ResponseEntity.ok(
+ Map.of(
+ "user", buildUserResponse(user),
+ "session", Map.of("access_token", token, "expires_in", 3600)));
} catch (AuthenticationException e) {
- log.error("Authentication failed for user: {}", request.getEmail(), e);
+ log.error("Authentication failed for user: {}", request.email(), e);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
- .body(Map.of("error", "Invalid credentials"));
+ .body(Map.of("error", "Invalid credentials"));
} catch (Exception e) {
- log.error("Login error for user: {}", request.getEmail(), e);
+ log.error("Login error for user: {}", request.email(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body(Map.of("error", "Internal server error"));
+ .body(Map.of("error", "Internal server error"));
}
}
@@ -116,53 +112,57 @@ public class AuthController {
@PostMapping("/register")
public ResponseEntity> register(@RequestBody RegisterRequest request) {
try {
- log.debug("Registration attempt for user: {}", request.getEmail());
+ log.debug("Registration attempt for user: {}", request.email());
// Check if username exists
- if (userService.usernameExistsIgnoreCase(request.getEmail())) {
- log.warn("Registration failed: username already exists: {}", request.getEmail());
+ if (userService.usernameExistsIgnoreCase(request.email())) {
+ log.warn("Registration failed: username already exists: {}", request.email());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
- .body(Map.of("error", "User already exists"));
+ .body(Map.of("error", "User already exists"));
}
// Validate username format
- if (!userService.isUsernameValid(request.getEmail())) {
- log.warn("Registration failed: invalid username format: {}", request.getEmail());
+ if (!userService.isUsernameValid(request.email())) {
+ log.warn("Registration failed: invalid username format: {}", request.email());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
- .body(Map.of("error", "Invalid username format"));
+ .body(Map.of("error", "Invalid username format"));
}
// Validate password
- if (request.getPassword() == null || request.getPassword().length() < 6) {
+ if (request.password() == null || request.password().length() < 6) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
- .body(Map.of("error", "Password must be at least 6 characters"));
+ .body(Map.of("error", "Password must be at least 6 characters"));
}
// Create user (using default team and USER role)
- User user = userService.saveUser(
- request.getEmail(),
- request.getPassword(),
- (Long) null, // team (use default)
- Role.USER.getRoleId(),
- false // first login not required
- );
+ User user =
+ userService.saveUser(
+ request.email(),
+ request.password(),
+ (Long) null, // team (use default)
+ Role.USER.getRoleId(),
+ false // first login not required
+ );
- log.info("User registered successfully: {}", request.getEmail());
+ log.info("User registered successfully: {}", request.email());
// Return user info (Note: No session, user must login)
- return ResponseEntity.status(HttpStatus.CREATED).body(Map.of(
- "user", buildUserResponse(user),
- "message", "Account created successfully. Please log in."
- ));
+ return ResponseEntity.status(HttpStatus.CREATED)
+ .body(
+ Map.of(
+ "user",
+ buildUserResponse(user),
+ "message",
+ "Account created successfully. Please log in."));
} catch (IllegalArgumentException e) {
log.error("Registration validation error: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
- .body(Map.of("error", e.getMessage()));
+ .body(Map.of("error", e.getMessage()));
} catch (Exception e) {
- log.error("Registration error for user: {}", request.getEmail(), e);
+ log.error("Registration error for user: {}", request.email(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body(Map.of("error", "Registration failed: " + e.getMessage()));
+ .body(Map.of("error", "Registration failed: " + e.getMessage()));
}
}
@@ -176,22 +176,22 @@ public class AuthController {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
- if (auth == null || !auth.isAuthenticated() || auth.getPrincipal().equals("anonymousUser")) {
+ if (auth == null
+ || !auth.isAuthenticated()
+ || auth.getPrincipal().equals("anonymousUser")) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
- .body(Map.of("error", "Not authenticated"));
+ .body(Map.of("error", "Not authenticated"));
}
UserDetails userDetails = (UserDetails) auth.getPrincipal();
User user = (User) userDetails;
- return ResponseEntity.ok(Map.of(
- "user", buildUserResponse(user)
- ));
+ return ResponseEntity.ok(Map.of("user", buildUserResponse(user)));
} catch (Exception e) {
log.error("Get current user error", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body(Map.of("error", "Internal server error"));
+ .body(Map.of("error", "Internal server error"));
}
}
@@ -217,7 +217,7 @@ public class AuthController {
} catch (Exception e) {
log.error("Logout error", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body(Map.of("error", "Internal server error"));
+ .body(Map.of("error", "Internal server error"));
}
}
@@ -229,15 +229,13 @@ public class AuthController {
* @return New token information
*/
@PostMapping("/refresh")
- public ResponseEntity> refresh(
- HttpServletRequest request,
- HttpServletResponse response) {
+ public ResponseEntity> refresh(HttpServletRequest request, HttpServletResponse response) {
try {
String token = jwtService.extractToken(request);
if (token == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
- .body(Map.of("error", "No token found"));
+ .body(Map.of("error", "No token found"));
}
// Validate and extract username
@@ -257,15 +255,12 @@ public class AuthController {
log.debug("Token refreshed for user: {}", username);
- return ResponseEntity.ok(Map.of(
- "access_token", newToken,
- "expires_in", 3600
- ));
+ return ResponseEntity.ok(Map.of("access_token", newToken, "expires_in", 3600));
} catch (Exception e) {
log.error("Token refresh error", e);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
- .body(Map.of("error", "Token refresh failed"));
+ .body(Map.of("error", "Token refresh failed"));
}
}
@@ -295,13 +290,9 @@ public class AuthController {
// Request/Response DTOs
// ===========================
- /**
- * Login request DTO
- */
+ /** Login request DTO */
public record LoginRequest(String email, String password) {}
- /**
- * Registration request DTO
- */
+ /** Registration request DTO */
public record RegisterRequest(String email, String password, String name) {}
-}
\ No newline at end of file
+}
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 faf50832fa..d1e57cc58e 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,14 +75,23 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String jwtToken = jwtService.extractToken(request);
if (jwtToken == null) {
- // Any unauthenticated requests should redirect to /login
+ // Allow auth endpoints to pass through without JWT
String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();
- if (!requestURI.startsWith(contextPath + "/login")) {
+ // 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;
}
+
+ // For auth endpoints without JWT, continue to the endpoint
+ // (it will return 401 if needed)
+ if (requestURI.startsWith(contextPath + "/api/v1/auth")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
}
try {
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 f51a9d5430..5b2b3b2810 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
@@ -236,6 +236,9 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
contextPath + "/pdfjs/",
contextPath + "/pdfjs-legacy/",
contextPath + "/api/v1/info/status",
+ contextPath + "/api/v1/auth/login",
+ contextPath + "/api/v1/auth/register",
+ contextPath + "/api/v1/auth/refresh",
contextPath + "/site.webmanifest"
};
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 8724da9a83..06dd81c951 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
@@ -279,9 +279,12 @@ public class JwtService implements JwtServiceInterface {
ResponseCookie.from(JWT_COOKIE_NAME, Newlines.stripAll(token))
.httpOnly(true)
.secure(secureCookie)
- .sameSite("Strict")
+ .sameSite("Lax") // Changed from Strict to Lax for cross-port dev
+ // compatibility
.maxAge(EXPIRATION / 1000)
.path("/")
+ .domain("localhost") // Set domain to localhost for cross-port dev
+ // compatibility
.build();
response.addHeader("Set-Cookie", cookie.toString());
@@ -296,6 +299,8 @@ public class JwtService implements JwtServiceInterface {
.sameSite("None")
.maxAge(0)
.path("/")
+ .domain("localhost") // Set domain to localhost for cross-port dev
+ // compatibility
.build();
response.addHeader("Set-Cookie", cookie.toString());
diff --git a/frontend/public/Login/AddToPDF.png b/frontend/public/Login/AddToPDF.png
new file mode 100644
index 0000000000..94e9a0dedf
Binary files /dev/null and b/frontend/public/Login/AddToPDF.png differ
diff --git a/frontend/public/Login/Firstpage.png b/frontend/public/Login/Firstpage.png
new file mode 100644
index 0000000000..f12133f4f7
Binary files /dev/null and b/frontend/public/Login/Firstpage.png differ
diff --git a/frontend/public/Login/LoginBackgroundPanel.png b/frontend/public/Login/LoginBackgroundPanel.png
new file mode 100644
index 0000000000..4ea0e0ccf1
Binary files /dev/null and b/frontend/public/Login/LoginBackgroundPanel.png differ
diff --git a/frontend/public/Login/SecurePDF.png b/frontend/public/Login/SecurePDF.png
new file mode 100644
index 0000000000..6184440e92
Binary files /dev/null and b/frontend/public/Login/SecurePDF.png differ
diff --git a/frontend/public/Login/apple.svg b/frontend/public/Login/apple.svg
new file mode 100644
index 0000000000..b947f4b6bc
--- /dev/null
+++ b/frontend/public/Login/apple.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/public/Login/azure.svg b/frontend/public/Login/azure.svg
new file mode 100644
index 0000000000..fc1130cbb2
--- /dev/null
+++ b/frontend/public/Login/azure.svg
@@ -0,0 +1,6 @@
+
diff --git a/frontend/public/Login/github.svg b/frontend/public/Login/github.svg
new file mode 100644
index 0000000000..651eaac2b8
--- /dev/null
+++ b/frontend/public/Login/github.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/public/Login/google.svg b/frontend/public/Login/google.svg
new file mode 100644
index 0000000000..27e4a4ac9c
--- /dev/null
+++ b/frontend/public/Login/google.svg
@@ -0,0 +1,14 @@
+
diff --git a/frontend/public/Login/microsoft.svg b/frontend/public/Login/microsoft.svg
new file mode 100644
index 0000000000..fc1130cbb2
--- /dev/null
+++ b/frontend/public/Login/microsoft.svg
@@ -0,0 +1,6 @@
+
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index c9736c04de..5b0d061da2 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,5 @@
import { Suspense } from "react";
-import { BrowserRouter, Routes, Route } from "react-router-dom";
+import { Routes, Route } from "react-router-dom";
import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider";
import { FileContextProvider } from "./contexts/FileContext";
import { NavigationProvider } from "./contexts/NavigationContext";
@@ -52,8 +52,7 @@ const LoadingFallback = () => (
export default function App() {
return (
+ )
+ }
+
+ return (
+
+ )}
+
+ {/* Image slides */}
+ {imageSlides.map((s, idx) => (
+