Frontend: design system, navigation, auth guard, signup screen #33

Merged
marcel merged 25 commits from feat/issue-16-design-system into master 2026-04-02 19:00:19 +02:00
6 changed files with 42 additions and 16 deletions
Showing only changes of commit 0aa65214fc - Show all commits

View File

@@ -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);

View File

@@ -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()

View File

@@ -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 }) => {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>