Remove duplicated private authenticateInSession from AuthController and HouseholdController. Add a single public implementation on AuthService with session fixation protection built in. HouseholdController now injects AuthService and passes role "user" for invite-accepted accounts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
122 lines
5.5 KiB
Java
122 lines
5.5 KiB
Java
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()));
|
|
}
|
|
}
|