AuthenticationController.java

package no.ntnu.idi.stud.savingsapp.controller.authentication;

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.security.SecurityRequirements;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import lombok.extern.slf4j.Slf4j;
import no.ntnu.idi.stud.savingsapp.dto.auth.AuthenticationResponse;
import no.ntnu.idi.stud.savingsapp.dto.auth.BankIDRequest;
import no.ntnu.idi.stud.savingsapp.dto.auth.LoginRequest;
import no.ntnu.idi.stud.savingsapp.dto.auth.SignUpRequest;
import no.ntnu.idi.stud.savingsapp.exception.ExceptionResponse;
import no.ntnu.idi.stud.savingsapp.exception.auth.InvalidCredentialsException;
import no.ntnu.idi.stud.savingsapp.exception.user.EmailAlreadyExistsException;
import no.ntnu.idi.stud.savingsapp.exception.user.UserNotFoundException;
import no.ntnu.idi.stud.savingsapp.model.user.User;
import no.ntnu.idi.stud.savingsapp.service.UserService;
import no.ntnu.idi.stud.savingsapp.utils.TokenUtils;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * Controller handling authentication related requests.
 */
@RestController
@RequestMapping("/api/auth")
@EnableAutoConfiguration
@Validated
@Tag(name = "Authentication")
@Slf4j
public class AuthenticationController {

	@Autowired
	private UserService userService;

	@Autowired
	private ModelMapper modelMapper;

	/**
	 * Handles user login requests.
	 * @param request The login request.
	 * @return ResponseEntity containing the authentication response.
	 * @throws InvalidCredentialsException if the provided credentials are invalid.
	 * @throws UserNotFoundException if the user is not found.
	 */
	@Operation(summary = "User Login", description = "Log in with an existing user")
	@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully logged in"),
			@ApiResponse(responseCode = "401", description = "Invalid credentials",
					content = @Content(schema = @Schema(implementation = ExceptionResponse.class))),
			@ApiResponse(responseCode = "404", description = "User not found",
					content = @Content(schema = @Schema(implementation = ExceptionResponse.class))) })
	@SecurityRequirements
	@PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE,
			consumes = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<AuthenticationResponse> login(@RequestBody @Valid LoginRequest request) {
		User user = userService.login(request.getEmail(), request.getPassword());
		String token = TokenUtils.generateToken(user);
		log.info("[AuthenticationController:login] Successfully logged in");
		return ResponseEntity.ok(new AuthenticationResponse(user.getFirstName(), user.getLastName(), user.getId(),
				user.getProfileImage(), user.getRole().name(), user.getSubscriptionLevel().name(), token));
	}

	/**
	 * Handles user signup requests.
	 * @param request The signup request.
	 * @return ResponseEntity containing the authentication response.
	 * @throws EmailAlreadyExistsException if the email is registered with an existing
	 * user.
	 */
	@Operation(summary = "User Signup", description = "Sign up a new user")
	@ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Successfully signed up"),
			@ApiResponse(responseCode = "409", description = "Email already exists",
					content = @Content(schema = @Schema(implementation = ExceptionResponse.class))) })
	@SecurityRequirements
	@PostMapping(value = "/signup", produces = MediaType.APPLICATION_JSON_VALUE,
			consumes = MediaType.APPLICATION_JSON_VALUE)
	@ResponseStatus(value = HttpStatus.CREATED)
	public ResponseEntity<AuthenticationResponse> signup(@RequestBody @Valid SignUpRequest request) {
		User requestUser = modelMapper.map(request, User.class);
		User user = userService.register(requestUser);
		String token = TokenUtils.generateToken(user);
		log.info("[AuthenticationController:signup] Successfully signed up");
		return ResponseEntity.status(HttpStatus.CREATED)
			.body(new AuthenticationResponse(user.getFirstName(), user.getLastName(), user.getId(),
					user.getProfileImage(), user.getRole().name(), user.getSubscriptionLevel().name(), token));
	}

	/**
	 * Handles authentication requests using BankID. This method processes an
	 * authentication request by taking a unique code and state from the BankID request,
	 * verifies the user, and returns an authentication token along with user details.
	 * @param request The request body containing the authentication details required by
	 * BankID, specifically a 'code' and 'state' used for user verification.
	 * @return A ResponseEntity object containing the user's authentication details
	 * including a JWT token if the authentication is successful.
	 * @apiNote This method is protected by security requirements that must be met before
	 * the authentication can proceed.
	 */
	@Operation(summary = "Authenticate a BankID request", description = "Authenticate a BankID request")
	@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "If the authentication is successful") })
	@SecurityRequirements
	@PostMapping(value = "/bank-id")
	public ResponseEntity<AuthenticationResponse> bankIdAuthentication(@RequestBody BankIDRequest request) {
		User user = userService.bankIDAuth(request.getCode(), request.getState());
		String token = TokenUtils.generateToken(user);
		log.info("[AuthenticationController:bankIdAuthentication] BankId Authenticated successfully");
		return ResponseEntity.status(HttpStatus.CREATED)
			.body(new AuthenticationResponse(user.getFirstName(), user.getLastName(), user.getId(),
					user.getProfileImage(), user.getRole().name(), user.getSubscriptionLevel().name(), token));
	}

	/**
	 * Validates an email.
	 * @param email The email.
	 * @return ResponseEntity.
	 * @throws EmailAlreadyExistsException if the email is registered with an existing
	 * user.
	 */
	@Operation(summary = "Validate email", description = "Check that the given email is valid")
	@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Email is valid"),
			@ApiResponse(responseCode = "409", description = "Email already exists",
					content = @Content(schema = @Schema(implementation = ExceptionResponse.class))) })
	@SecurityRequirements
	@PostMapping(value = "/valid-email/{email}")
	public ResponseEntity<?> validateEmail(@PathVariable @Email(message = "Invalid email") String email) {
		try {
			userService.findByEmail(email);
			log.error("[AuthenticationController:validateEmail] email already exists: {}", email);
			throw new EmailAlreadyExistsException();
		}
		catch (UserNotFoundException e) {
			log.info("[AuthenticationController:validateEmail] email is valid: {}", email);
			return ResponseEntity.ok().build();
		}
	}

}