package com.recipeapp.auth; import com.recipeapp.auth.dto.*; import com.recipeapp.auth.entity.UserAccount; import com.recipeapp.common.ConflictException; import com.recipeapp.common.ResourceNotFoundException; import com.recipeapp.common.ValidationException; import com.recipeapp.household.HouseholdMemberRepository; import com.recipeapp.household.entity.HouseholdMember; import jakarta.servlet.http.HttpServletRequest; 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.crypto.password.PasswordEncoder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service public class AuthService { private final UserAccountRepository userAccountRepository; private final HouseholdMemberRepository householdMemberRepository; private final PasswordEncoder passwordEncoder; public AuthService(UserAccountRepository userAccountRepository, HouseholdMemberRepository householdMemberRepository, PasswordEncoder passwordEncoder) { this.userAccountRepository = userAccountRepository; this.householdMemberRepository = householdMemberRepository; this.passwordEncoder = passwordEncoder; } @Transactional public UserResponse signup(SignupRequest request) { if (userAccountRepository.existsByEmailIgnoreCase(request.email())) { throw new ConflictException("Email already registered"); } var user = new UserAccount( request.email(), request.displayName(), passwordEncoder.encode(request.password()) ); user = userAccountRepository.save(user); return UserResponse.basic(user.getId(), user.getEmail(), user.getDisplayName()); } @Transactional(readOnly = true) public UserResponse login(LoginRequest request) { UserAccount user = userAccountRepository.findByEmailIgnoreCase(request.email()) .orElseThrow(() -> new ResourceNotFoundException("Invalid email or password")); if (!user.isActive()) { throw new ValidationException("Account is deactivated"); } if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { throw new ValidationException("Invalid email or password"); } return toUserResponse(user); } @Transactional(readOnly = true) public UserResponse getCurrentUser(String email) { UserAccount user = userAccountRepository.findByEmailIgnoreCase(email) .orElseThrow(() -> new ResourceNotFoundException("User not found")); return toUserResponse(user); } @Transactional public UserResponse updateProfile(String email, UpdateProfileRequest request) { UserAccount user = userAccountRepository.findByEmailIgnoreCase(email) .orElseThrow(() -> new ResourceNotFoundException("User not found")); if (request.displayName() != null) { user.setDisplayName(request.displayName()); } if (request.newPassword() != null) { if (request.currentPassword() == null) { throw new ValidationException("Current password is required to set a new password"); } if (!passwordEncoder.matches(request.currentPassword(), user.getPasswordHash())) { throw new ValidationException("Current password is incorrect"); } user.setPasswordHash(passwordEncoder.encode(request.newPassword())); } user = userAccountRepository.save(user); return UserResponse.basic(user.getId(), user.getEmail(), user.getDisplayName()); } /** * Establishes an authenticated Spring Security session for the given user. * Invalidates any existing session first (session fixation protection). */ public void authenticateInSession(String email, String role, HttpServletRequest request) { var oldSession = request.getSession(false); if (oldSession != null) { oldSession.invalidate(); } 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); } private UserResponse toUserResponse(UserAccount user) { return householdMemberRepository.findByUserEmailIgnoreCase(user.getEmail()) .map(member -> UserResponse.withHousehold( user.getId(), user.getEmail(), user.getDisplayName(), member.getHousehold().getId(), member.getHousehold().getName(), member.getRole(), user.getSystemRole())) .orElse(UserResponse.withHousehold( user.getId(), user.getEmail(), user.getDisplayName(), null, null, null, user.getSystemRole())); } }