756a80c5b9
Signed-off-by: Alan Brault <alan.brault@visus.io>
160 lines
6.2 KiB
Kotlin
160 lines
6.2 KiB
Kotlin
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<AuthResponse> =
|
|
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<Unit> {
|
|
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<AuthResponse> {
|
|
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<RegisterResponse> {
|
|
val user = authService.register(request.email, request.password)
|
|
return ResponseEntity.status(201).body(user)
|
|
}
|
|
}
|