Frontend: design system, navigation, auth guard, signup screen #33
@@ -7,9 +7,15 @@ import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/auth")
|
||||
@@ -26,7 +32,7 @@ public class AuthController {
|
||||
@Valid @RequestBody SignupRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
UserResponse user = authService.signup(request);
|
||||
httpRequest.getSession(true);
|
||||
authenticateInSession(user.email(), "user", httpRequest);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(user));
|
||||
}
|
||||
|
||||
@@ -35,11 +41,25 @@ public class AuthController {
|
||||
@Valid @RequestBody LoginRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
UserResponse user = authService.login(request);
|
||||
HttpSession session = httpRequest.getSession(true);
|
||||
session.setAttribute("user_email", user.email());
|
||||
// Session fixation protection: invalidate old session before creating new one
|
||||
var oldSession = httpRequest.getSession(false);
|
||||
if (oldSession != null) {
|
||||
oldSession.invalidate();
|
||||
}
|
||||
authenticateInSession(user.email(), user.systemRole() != null ? user.systemRole() : "user", httpRequest);
|
||||
return ResponseEntity.ok(ApiResponse.success(user));
|
||||
}
|
||||
|
||||
private void authenticateInSession(String email, String role, HttpServletRequest request) {
|
||||
var auth = UsernamePasswordAuthenticationToken.authenticated(
|
||||
email, null, List.of(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(auth);
|
||||
SecurityContextHolder.setContext(context);
|
||||
request.getSession(true).setAttribute(
|
||||
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context);
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<Void> logout(HttpServletRequest httpRequest) {
|
||||
HttpSession session = httpRequest.getSession(false);
|
||||
|
||||
@@ -10,8 +10,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -20,9 +18,7 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf
|
||||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
||||
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/v1/auth/signup", "/v1/auth/login").permitAll()
|
||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||
|
||||
@@ -15,7 +15,7 @@ function isPublicRoute(pathname: string): boolean {
|
||||
|
||||
function loginRedirect(pathname: string): never {
|
||||
const target = '/login?redirect=' + encodeURIComponent(pathname);
|
||||
redirect(302, target);
|
||||
throw redirect(302, target);
|
||||
}
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { apiClient } from '$lib/server/api';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request, url, fetch }) => {
|
||||
default: async ({ request, url, fetch, cookies }) => {
|
||||
const formData = await request.formData();
|
||||
const email = (formData.get('email') ?? '').toString().trim();
|
||||
const password = (formData.get('password') ?? '').toString();
|
||||
@@ -24,7 +24,7 @@ export const actions = {
|
||||
}
|
||||
|
||||
const api = apiClient(fetch);
|
||||
const { error } = await api.POST('/v1/auth/login', {
|
||||
const { error, response } = await api.POST('/v1/auth/login', {
|
||||
body: { email, password }
|
||||
});
|
||||
|
||||
@@ -35,7 +35,12 @@ export const actions = {
|
||||
});
|
||||
}
|
||||
|
||||
const sessionId = response.headers.get('set-cookie')?.match(/JSESSIONID=([^;]+)/i)?.[1];
|
||||
if (sessionId) {
|
||||
cookies.set('JSESSIONID', sessionId, { path: '/', httpOnly: true, sameSite: 'lax' });
|
||||
}
|
||||
|
||||
const redirectTo = url.searchParams.get('redirect') || '/planner';
|
||||
redirect(303, redirectTo);
|
||||
throw redirect(303, redirectTo);
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { apiClient } from '$lib/server/api';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions = {
|
||||
default: async ({ request, fetch }) => {
|
||||
default: async ({ request, fetch, cookies }) => {
|
||||
const formData = await request.formData();
|
||||
const displayName = (formData.get('displayName') ?? '').toString().trim();
|
||||
const email = (formData.get('email') ?? '').toString().trim();
|
||||
@@ -29,7 +29,7 @@ export const actions = {
|
||||
}
|
||||
|
||||
const api = apiClient(fetch);
|
||||
const { error } = await api.POST('/v1/auth/signup', {
|
||||
const { error, response } = await api.POST('/v1/auth/signup', {
|
||||
body: { displayName, email, password }
|
||||
});
|
||||
|
||||
@@ -41,6 +41,11 @@ export const actions = {
|
||||
});
|
||||
}
|
||||
|
||||
redirect(303, '/household/setup');
|
||||
const sessionId = response.headers.get('set-cookie')?.match(/JSESSIONID=([^;]+)/i)?.[1];
|
||||
if (sessionId) {
|
||||
cookies.set('JSESSIONID', sessionId, { path: '/', httpOnly: true, sameSite: 'lax' });
|
||||
}
|
||||
|
||||
throw redirect(303, '/household/setup');
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- Mobile: stacked, Desktop: side by side -->
|
||||
<div class="flex min-h-screen flex-col md:flex-row">
|
||||
<BrandPanel />
|
||||
<div class="flex flex-1 flex-col items-start justify-center px-[20px] py-[24px] md:items-center md:px-[56px] md:py-[48px]">
|
||||
<div class="flex flex-1 flex-col items-center justify-center px-[20px] py-[24px] md:px-[56px] md:py-[48px]">
|
||||
<div class="w-full max-w-[380px]">
|
||||
<SignupForm {form} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user