1
0

chore: add custom error handlers

Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
2026-03-28 08:00:34 -04:00
parent 7c742fca14
commit be59dcb2bf
5 changed files with 75 additions and 7 deletions
@@ -10,7 +10,7 @@ import org.springframework.web.reactive.config.WebFluxConfigurer
@Configuration
class ApiVersionConfig : WebFluxConfigurer {
override fun configurePathMatching(configurer: PathMatchConfigurer) {
configurer.addPathPrefix("/api/v1") { controller ->
configurer.addPathPrefix("/v1") { controller ->
val apiVersion = AnnotatedElementUtils.findMergedAnnotation(controller, ApiVersion::class.java)
AnnotatedElementUtils.hasAnnotation(controller, RestController::class.java) && apiVersion?.version == 1
}
@@ -36,6 +36,6 @@ class OpenApiConfig {
GroupedOpenApi
.builder()
.group("v1")
.pathsToMatch("/api/v1/**")
.pathsToMatch("/v1/**")
.build()
}
@@ -1,6 +1,8 @@
package io.visus.demos.kotlinapi.config
import io.visus.demos.kotlinapi.domain.service.UserService
import io.visus.demos.kotlinapi.infrastructure.security.CustomAccessDeniedHandler
import io.visus.demos.kotlinapi.infrastructure.security.CustomAuthenticationEntryPoint
import io.visus.demos.kotlinapi.infrastructure.security.JwtAuthenticationFilter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -24,6 +26,8 @@ import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
class SecurityConfig(
private val jwtAuthenticationFilter: JwtAuthenticationFilter,
private val userDetailsService: UserService,
private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint,
private val customAccessDeniedHandler: CustomAccessDeniedHandler,
) {
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
@@ -56,16 +60,20 @@ class SecurityConfig(
.csrf { it.disable() }
.cors { it.configurationSource(corsConfigurationSource()) }
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange { authorize ->
.exceptionHandling { handlers ->
handlers
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
}.authorizeExchange { authorize ->
authorize
.pathMatchers(
"/swagger-ui.html",
"/swagger-ui/**",
"/api-docs/**",
"/api/v1/health",
"/api/v1/auth/login",
"/api/v1/auth/refresh",
"/api/v1/auth/register",
"/v1/health",
"/v1/auth/login",
"/v1/auth/refresh",
"/v1/auth/register",
).permitAll()
.anyExchange()
.authenticated()
@@ -0,0 +1,30 @@
package io.visus.demos.kotlinapi.infrastructure.security
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
@Component
class CustomAccessDeniedHandler : ServerAccessDeniedHandler {
override fun handle(
exchange: ServerWebExchange,
denied: AccessDeniedException,
): Mono<Void> =
Mono
.fromRunnable<Void> {
exchange.response.statusCode = HttpStatus.FORBIDDEN
exchange.response.headers.contentType = MediaType.APPLICATION_JSON
}.then(
exchange.response.writeWith(
Mono.just(
exchange.response.bufferFactory().wrap(
"""{"error":"Forbidden","message":"Access denied"}""".toByteArray(),
),
),
),
)
}
@@ -0,0 +1,30 @@
package io.visus.demos.kotlinapi.infrastructure.security
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.server.ServerAuthenticationEntryPoint
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
@Component
class CustomAuthenticationEntryPoint : ServerAuthenticationEntryPoint {
override fun commence(
exchange: ServerWebExchange,
ex: AuthenticationException,
): Mono<Void> =
Mono
.fromRunnable<Void> {
exchange.response.statusCode = HttpStatus.UNAUTHORIZED
exchange.response.headers.contentType = MediaType.APPLICATION_JSON
}.then(
exchange.response.writeWith(
Mono.just(
exchange.response.bufferFactory().wrap(
"""{"error":"Unauthorized","message":"Authentication required"}""".toByteArray(),
),
),
),
)
}