package mk.finki.roomreservation.service; import mk.finki.roomreservation.model.ActivateAccountRequest; import mk.finki.roomreservation.model.LoginRequest; import mk.finki.roomreservation.model.RegisterRequest; import mk.finki.roomreservation.model.UserOption; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.regex.Pattern; @Service public class AuthService { private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"); private final JdbcTemplate jdbcTemplate; private final BCryptPasswordEncoder passwordEncoder; public AuthService(JdbcTemplate jdbcTemplate, BCryptPasswordEncoder passwordEncoder) { this.jdbcTemplate = jdbcTemplate; this.passwordEncoder = passwordEncoder; } public UserOption login(LoginRequest request) { validateLoginRequest(request); UserOption user = findUserByIdentifier(request.identifier()); String passwordHash = findPasswordHash(user.userId()); if (!passwordEncoder.matches(request.password(), passwordHash)) { throw new IllegalArgumentException("Invalid username/email or password."); } return user; } @Transactional public UserOption register(RegisterRequest request) { validateRegisterRequest(request); String fullName = request.fullName().trim(); String username = request.username().trim(); String email = request.email().trim(); if (usernameExists(username)) { throw new IllegalArgumentException("Username is already taken."); } if (emailExists(email)) { throw new IllegalArgumentException("Email is already registered."); } Integer userId = jdbcTemplate.queryForObject( """ INSERT INTO project.users ( username, email, full_name, role ) VALUES (?, ?, ?, 'regular') RETURNING user_id """, Integer.class, username, email, fullName ); if (userId == null) { throw new RuntimeException("User registration failed."); } String passwordHash = passwordEncoder.encode(request.password()); jdbcTemplate.update( """ INSERT INTO project.user_credentials ( user_id, password_hash ) VALUES (?, ?) """, userId, passwordHash ); return findUserById(userId); } @Transactional public UserOption activateExistingAccount(ActivateAccountRequest request) { validateActivationRequest(request); UserOption user = findUserByIdentifier(request.identifier()); if (credentialsExist(user.userId())) { throw new IllegalArgumentException("This account already has a password."); } String passwordHash = passwordEncoder.encode(request.password()); jdbcTemplate.update( """ INSERT INTO project.user_credentials ( user_id, password_hash ) VALUES (?, ?) """, user.userId(), passwordHash ); return user; } private void validateLoginRequest(LoginRequest request) { if (request == null) { throw new IllegalArgumentException("Login request is required."); } if (request.identifier() == null || request.identifier().isBlank()) { throw new IllegalArgumentException("Username or email is required."); } if (request.password() == null || request.password().isBlank()) { throw new IllegalArgumentException("Password is required."); } } private void validateRegisterRequest(RegisterRequest request) { if (request == null) { throw new IllegalArgumentException("Register request is required."); } if (request.fullName() == null || request.fullName().isBlank()) { throw new IllegalArgumentException("Full name is required."); } if (request.username() == null || request.username().isBlank()) { throw new IllegalArgumentException("Username is required."); } if (request.email() == null || request.email().isBlank()) { throw new IllegalArgumentException("Email is required."); } if (!EMAIL_PATTERN.matcher(request.email().trim()).matches()) { throw new IllegalArgumentException("Email format is not valid."); } validatePassword(request.password()); } private void validateActivationRequest(ActivateAccountRequest request) { if (request == null) { throw new IllegalArgumentException("Activation request is required."); } if (request.identifier() == null || request.identifier().isBlank()) { throw new IllegalArgumentException("Username or email is required."); } validatePassword(request.password()); } private void validatePassword(String password) { if (password == null || password.isBlank()) { throw new IllegalArgumentException("Password is required."); } if (password.length() < 8) { throw new IllegalArgumentException("Password must contain at least 8 characters."); } } private UserOption findUserByIdentifier(String identifier) { List users = jdbcTemplate.query( """ SELECT user_id, username, email, full_name, role FROM project.users WHERE LOWER(username) = LOWER(?) OR LOWER(email) = LOWER(?) ORDER BY user_id LIMIT 1 """, (rs, rowNum) -> new UserOption( rs.getInt("user_id"), rs.getString("username"), rs.getString("email"), rs.getString("full_name"), rs.getString("role") ), identifier.trim(), identifier.trim() ); if (users.isEmpty()) { throw new IllegalArgumentException("Invalid username/email or password."); } return users.get(0); } private UserOption findUserById(int userId) { return jdbcTemplate.queryForObject( """ SELECT user_id, username, email, full_name, role FROM project.users WHERE user_id = ? """, (rs, rowNum) -> new UserOption( rs.getInt("user_id"), rs.getString("username"), rs.getString("email"), rs.getString("full_name"), rs.getString("role") ), userId ); } private String findPasswordHash(int userId) { List hashes = jdbcTemplate.query( """ SELECT password_hash FROM project.user_credentials WHERE user_id = ? """, (rs, rowNum) -> rs.getString("password_hash"), userId ); if (hashes.isEmpty()) { throw new IllegalArgumentException("This account is not activated yet. Please request access."); } return hashes.get(0); } private boolean usernameExists(String username) { Long count = jdbcTemplate.queryForObject( """ SELECT COUNT(*) FROM project.users WHERE LOWER(username) = LOWER(?) """, Long.class, username ); return count != null && count > 0; } private boolean emailExists(String email) { Long count = jdbcTemplate.queryForObject( """ SELECT COUNT(*) FROM project.users WHERE LOWER(email) = LOWER(?) """, Long.class, email ); return count != null && count > 0; } private boolean credentialsExist(int userId) { Long count = jdbcTemplate.queryForObject( """ SELECT COUNT(*) FROM project.user_credentials WHERE user_id = ? """, Long.class, userId ); return count != null && count > 0; } }