AuthController.java

package com.wilzwert.myjobs.infrastructure.api.rest.controller;


import com.wilzwert.myjobs.core.domain.model.user.command.RegisterUserCommand;
import com.wilzwert.myjobs.core.domain.model.user.User;
import com.wilzwert.myjobs.core.domain.model.user.UserId;
import com.wilzwert.myjobs.core.domain.model.user.ports.driving.CheckUserAvailabilityUseCase;
import com.wilzwert.myjobs.core.domain.model.user.ports.driving.LoginUseCase;
import com.wilzwert.myjobs.core.domain.model.user.ports.driving.RegisterUseCase;
import com.wilzwert.myjobs.core.domain.model.user.ports.driven.UserDataManager;
import com.wilzwert.myjobs.infrastructure.api.rest.dto.*;
import com.wilzwert.myjobs.infrastructure.persistence.mongo.mapper.UserMapper;
import com.wilzwert.myjobs.infrastructure.security.captcha.RequiresCaptcha;
import com.wilzwert.myjobs.infrastructure.security.jwt.JwtAuthenticatedUser;
import com.wilzwert.myjobs.infrastructure.security.model.RefreshToken;
import com.wilzwert.myjobs.infrastructure.security.service.CookieService;
import com.wilzwert.myjobs.infrastructure.security.service.JwtService;
import com.wilzwert.myjobs.infrastructure.security.service.RefreshTokenService;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

/**
 * @author Wilhelm Zwertvaegher
 */
@RestController
@Slf4j
@RequestMapping("/api/auth")
public class AuthController {

    private final RegisterUseCase registerUseCase;

    private final LoginUseCase loginUseCase;

    private final CheckUserAvailabilityUseCase checkUserAvailabilityUseCase;

    private final UserMapper userMapper;

    private final CookieService cookieService;

    private final RefreshTokenService refreshTokenService;

    private final UserDataManager userDataManager;

    private final JwtService jwtService;

    public AuthController(RegisterUseCase registerUseCase, LoginUseCase loginUseCase, CheckUserAvailabilityUseCase checkUserAvailabilityUseCase, UserMapper userMapper, CookieService cookieService, RefreshTokenService refreshTokenService, UserDataManager userDataManager, JwtService jwtService) {
        this.registerUseCase = registerUseCase;
        this.loginUseCase = loginUseCase;
        this.checkUserAvailabilityUseCase = checkUserAvailabilityUseCase;
        this.userMapper = userMapper;
        this.cookieService = cookieService;
        this.refreshTokenService = refreshTokenService;
        this.userDataManager = userDataManager;
        this.jwtService = jwtService;
    }

    @PostMapping("/register")
    @RequiresCaptcha
    public UserResponse register(@RequestBody @Valid final RegisterUserRequest registerUserRequest) {
        RegisterUserCommand registerUserCommand = userMapper.toCommand(registerUserRequest);
        return userMapper.toResponse(registerUseCase.registerUser(registerUserCommand));
    }

    @PostMapping("/logout")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public ResponseEntity<Void> logout() {
        var responseEntity = ResponseEntity.noContent();
        return responseEntity
                .header(HttpHeaders.SET_COOKIE, cookieService.revokeAccessTokenCookie().toString())
                .header(HttpHeaders.SET_COOKIE, cookieService.revokeRefreshTokenCookie().toString())
                .build();
    }

    @PostMapping("/login")
    @RequiresCaptcha
    public ResponseEntity<AuthResponse> login(@RequestBody @Valid final LoginRequest loginRequest) {
        log.info("User login with email {}", loginRequest.getEmail());
        try {
            log.info("User login - authenticating");
            // casting seems a bit ugly but in infra we actually know we will get a JwtAuthenticatedUser
            JwtAuthenticatedUser authenticatedUser = (JwtAuthenticatedUser) loginUseCase.authenticateUser(loginRequest.getEmail(), loginRequest.getPassword());
            var responseEntity = ResponseEntity.ok();
            return responseEntity
                    .header(HttpHeaders.SET_COOKIE, cookieService.createAccessTokenCookie(authenticatedUser.getJwtToken()).toString())
                    .header(HttpHeaders.SET_COOKIE, cookieService.createRefreshTokenCookie(authenticatedUser.getRefreshToken()).toString())
                    .body(new AuthResponse(authenticatedUser.getEmail(), authenticatedUser.getUsername(), authenticatedUser.getRole()));

        } catch (AuthenticationException e) {
            log.info("Login failed for User with email {}", loginRequest.getEmail());
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Login failed. " + e.getMessage());
        }
    }

    @GetMapping("/email-check")
    @RequiresCaptcha
    public ResponseEntity<Void> emailCheck(@RequestParam("email") String email) {
        return checkUserAvailabilityUseCase.isEmailTaken(email) ? new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY) : new ResponseEntity<>(HttpStatus.OK);
    }

    @GetMapping("/username-check")
    @RequiresCaptcha
    public ResponseEntity<Void> usernameCheck(@RequestParam("username") String username) {
        return checkUserAvailabilityUseCase.isUsernameTaken(username) ? new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY) : new ResponseEntity<>(HttpStatus.OK);
    }

    @PostMapping("/refresh-token")
    public ResponseEntity<AuthResponse> refreshAccessToken(@CookieValue(name = "refresh_token", required = false) String refreshToken) {
        if (refreshToken == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        log.debug("Refresh token {}", refreshToken);
        RefreshToken foundRefreshToken = refreshTokenService.findByToken(refreshToken).orElse(null);
        if (foundRefreshToken == null || !refreshTokenService.verifyExpiration(foundRefreshToken)) {
            log.debug("Found refresh token empty or expired {}", foundRefreshToken);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        User user = userDataManager.findById(new UserId(foundRefreshToken.getUserId())).orElse(null);
        if(user == null) {
            log.debug("Associated user not found for {}", refreshToken);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        log.debug("Delete previous refresh token and return a new one");
        refreshTokenService.deleteRefreshToken(foundRefreshToken);
        var newRefreshToken = refreshTokenService.createRefreshToken(user);
        var newAccessToken = jwtService.generateToken(user.getId().value().toString());
        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, cookieService.createAccessTokenCookie(newAccessToken).toString())
                .header(HttpHeaders.SET_COOKIE, cookieService.createRefreshTokenCookie(newRefreshToken.getToken()).toString())
                .body(new AuthResponse(user.getEmail(), user.getUsername(), user.getRole()));
    }
}