package io.visus.demos.kotlinapi.controller 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.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import io.visus.demos.kotlinapi.api.ApiVersion import io.visus.demos.kotlinapi.api.contracts.AuthResponse import io.visus.demos.kotlinapi.api.contracts.ErrorResponse import io.visus.demos.kotlinapi.api.contracts.LoginRequest import io.visus.demos.kotlinapi.api.contracts.RefreshTokenRequest import io.visus.demos.kotlinapi.api.contracts.RegisterRequest import io.visus.demos.kotlinapi.api.contracts.RegisterResponse import io.visus.demos.kotlinapi.domain.exception.InvalidTokenException import io.visus.demos.kotlinapi.domain.model.User import io.visus.demos.kotlinapi.domain.service.AuthService import jakarta.validation.Valid import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController import org.springframework.web.server.ServerWebExchange @RestController @ApiVersion(version = 1) @Tag(name = "Authentication", description = "Endpoints for user authentication and authorization") class AuthController( private val authService: AuthService, ) { @PostMapping("/auth/login", produces = [MediaType.APPLICATION_JSON_VALUE]) @Operation( summary = "Login", description = "Authenticate user with email and password, returns JWT tokens", security = [], ) @ApiResponses( value = [ ApiResponse( responseCode = "200", description = "Successful login, returns access and refresh tokens", content = [Content(schema = Schema(implementation = AuthResponse::class))], ), ApiResponse( responseCode = "400", description = "Invalid request payload", content = [Content(schema = Schema(implementation = ErrorResponse::class))], ), ApiResponse( responseCode = "401", description = "Unauthorized, invalid email or password", content = [Content(schema = Schema(implementation = ErrorResponse::class))], ), ], ) suspend fun login( @Valid @RequestBody request: LoginRequest, ): ResponseEntity = authService.login(request.email, request.password).let { ResponseEntity.ok(it) } @PostMapping("/auth/logout", produces = [MediaType.APPLICATION_JSON_VALUE]) @Operation( summary = "Logout", description = "Revokes all refresh tokens for the authenticated user", security = [SecurityRequirement(name = "bearerAuth")], ) @ApiResponses( value = [ ApiResponse( responseCode = "204", description = "Successfully logged out", ), ApiResponse( responseCode = "401", description = "Not authenticated", content = [Content(schema = Schema(implementation = ErrorResponse::class))], ), ], ) suspend fun logout( @AuthenticationPrincipal user: User, exchange: ServerWebExchange, ): ResponseEntity { val authHeader = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION) ?: throw InvalidTokenException("Authorization header is missing") val accessToken = authHeader.removePrefix("Bearer ") authService.logout(user.id!!, accessToken) return ResponseEntity.noContent().build() } @PostMapping("/auth/refresh", produces = [MediaType.APPLICATION_JSON_VALUE]) @Operation( summary = "Refresh access token", description = "Get a new access token using a valid refresh token", security = [], ) @ApiResponses( value = [ ApiResponse( responseCode = "200", description = "Successfully refreshed token", content = [Content(schema = Schema(implementation = AuthResponse::class))], ), ApiResponse( responseCode = "400", description = "Invalid request payload", content = [Content(schema = Schema(implementation = ErrorResponse::class))], ), ApiResponse( responseCode = "401", description = "Invalid or expired refresh token", content = [Content(schema = Schema(implementation = ErrorResponse::class))], ), ], ) suspend fun refreshToken( @Valid @RequestBody request: RefreshTokenRequest, ): ResponseEntity { val response = authService.refreshToken(request.refreshToken) return ResponseEntity.ok(response) } @PostMapping("/auth/register", produces = [MediaType.APPLICATION_JSON_VALUE]) @Operation( summary = "Register new user", description = "Creates a new user account", security = [], ) @ApiResponses( value = [ ApiResponse( responseCode = "201", description = "User successfully registered", content = [Content(schema = Schema(implementation = RegisterResponse::class))], ), ApiResponse( responseCode = "400", description = "Invalid request payload", content = [Content(schema = Schema(implementation = ErrorResponse::class))], ), ], ) suspend fun register( @Valid @RequestBody request: RegisterRequest, ): ResponseEntity { val user = authService.register(request.email, request.password) return ResponseEntity.status(201).body(user) } }