AuthenticationController.java

package api.controllers;

import ch.qos.logback.core.util.StringUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import api.dtos.AuthenticationDto;
import api.dtos.ErrorDto;
import api.dtos.LoginDto;
import api.dtos.RegisterDto;
import api.entities.RefreshToken;
import api.entities.User;
import api.exceptions.RefreshTokenException;
import api.services.AuthenticationService;
import api.services.TokenService;
import api.services.UserService;
import api.utils.CookieUtils;

/**
 * {@link AuthenticationController}.
 */
@RestController
@RequestMapping("/authenticate")
@Tag(name = "Authentication", description = "Handles user login and registration.")
public class AuthenticationController {
    @Autowired
    private AuthenticationService authenticationService;
    @Autowired
    private UserService userService;
    @Autowired
    private TokenService tokenService;
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * Login.
     *
     * @param login {@link LoginDto}
     * @return {@link ResponseEntity} {@link AuthenticationDto}
     */
    // region
    @Operation(summary = "Login", description = "Login to an existing user.")
    @ApiResponses({@ApiResponse(responseCode = "200",
        content = @Content(schema = @Schema(implementation = AuthenticationDto.class),
            mediaType = "application/json"))})
    // endregion
    @PostMapping("/login")
    public ResponseEntity<AuthenticationDto> login(@Valid @RequestBody LoginDto login) {
        User user = userService
            .find(StringUtil.isNullOrEmpty(login.getUsername()) ? login.getEmail() : login.getUsername()).orElse(null);

        Authentication authentication =
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user, login.getPassword()));

        RefreshToken refreshToken = tokenService.generateRefreshToken(user);
        AuthenticationDto authDto = tokenService.generateAccessToken(authentication.getName());

        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, CookieUtils.generateRefreshTokenCookie(refreshToken).toString())
            .body(authDto);
    }

    /**
     * Register.
     *
     * @param registerDto {@link RegisterDto}
     * @return {@link ResponseEntity} {@link AuthenticationDto}
     */
    // region
    @Operation(summary = "Register", description = "Register a new user.")
    @ApiResponses({
        @ApiResponse(responseCode = "200",
            content = @Content(schema = @Schema(implementation = AuthenticationDto.class),
                mediaType = "application/json")),
        @ApiResponse(responseCode = "400",
            content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json")),
        @ApiResponse(responseCode = "409",
            content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
    // endregion
    @PostMapping("/register")
    public ResponseEntity<AuthenticationDto> register(@Valid @RequestBody RegisterDto registerDto) {
        User user = authenticationService.register(registerDto);

        Authentication authentication = authenticationManager
            .authenticate(new UsernamePasswordAuthenticationToken(user, registerDto.getPassword()));

        RefreshToken refreshToken = tokenService.generateRefreshToken(user);
        AuthenticationDto authDto = tokenService.generateAccessToken(authentication.getName());

        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, CookieUtils.generateRefreshTokenCookie(refreshToken).toString())
            .body(authDto);
    }

    /**
     * Refresh a user.
     *
     * @param refreshToken {@link String}
     * @return {@link AuthenticationDto}
     */
    // region
    @Operation(summary = "Refresh", description = "Refresh a user.")
    @ApiResponses({
        @ApiResponse(responseCode = "200",
            content = @Content(schema = @Schema(implementation = AuthenticationDto.class),
                mediaType = "application/json")),
        @ApiResponse(responseCode = "401",
            content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
    // endregion
    @GetMapping("/refresh")
    public ResponseEntity<AuthenticationDto> refresh(
        @CookieValue(value = CookieUtils.REFRESH_COOKIE_NAME, required = false) String refreshToken) {
        if (refreshToken == null) {
            throw new RefreshTokenException();
        }

        RefreshToken regenToken = tokenService.validateAndRegenerate(refreshToken);
        AuthenticationDto authDto = tokenService.generateAccessToken(regenToken.getUser().getUsername());
        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, CookieUtils.generateRefreshTokenCookie(regenToken).toString())
            .body(authDto);
    }

    /**
     * Logout a user.
     *
     * @param refreshToken {@link String}
     * @return {@link ResponseEntity} {@link Void}
     */
    // region
    @Operation(summary = "Logout", description = "Logout a user.")
    @ApiResponses({@ApiResponse(responseCode = "204"),
        @ApiResponse(responseCode = "401",
            content = @Content(schema = @Schema(implementation = ErrorDto.class), mediaType = "application/json"))})
    // endregion
    @DeleteMapping("/logout")
    public ResponseEntity<Void> logout(
        @CookieValue(value = CookieUtils.REFRESH_COOKIE_NAME, required = false) String refreshToken) {
        if (refreshToken == null) {
            throw new RefreshTokenException();
        }

        tokenService.invalidate(refreshToken);
        return ResponseEntity.noContent().header(HttpHeaders.SET_COOKIE, CookieUtils.generateEmptyCookie().toString())
            .build();
    }
}