chore: clean up api, add user support
Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
@@ -42,10 +42,12 @@ dependencies {
|
|||||||
implementation("org.springframework.boot:spring-boot-starter-mongodb")
|
implementation("org.springframework.boot:spring-boot-starter-mongodb")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-restclient")
|
implementation("org.springframework.boot:spring-boot-starter-restclient")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
implementation("com.password4j:password4j:1.8.2")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
implementation("tech.mappie:mappie-api:2.2.21-2.1.1")
|
implementation("tech.mappie:mappie-api:2.2.21-2.1.1")
|
||||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4")
|
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-mongodb-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-mongodb-test")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-restclient-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-restclient-test")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-security-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-security-test")
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package io.visus.demos.kotlinapi.api
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class ApiVersion(
|
||||||
|
val version: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package io.visus.demos.kotlinapi.api
|
||||||
|
|
||||||
|
object HttpStatusCodes {
|
||||||
|
const val CONTINUE = "100"
|
||||||
|
const val SWITCHING_PROTOCOLS = "101"
|
||||||
|
const val PROCESSING = "102"
|
||||||
|
const val EARLY_HINTS = "103"
|
||||||
|
const val OK = "200"
|
||||||
|
const val CREATED = "201"
|
||||||
|
const val ACCEPTED = "202"
|
||||||
|
const val NON_AUTHORITATIVE_INFORMATION = "203"
|
||||||
|
const val NO_CONTENT = "204"
|
||||||
|
const val RESET_CONTENT = "205"
|
||||||
|
const val PARTIAL_CONTENT = "206"
|
||||||
|
const val MULTI_STATUS = "207"
|
||||||
|
const val ALREADY_REPORTED = "208"
|
||||||
|
const val IM_USED = "226"
|
||||||
|
const val MULTIPLE_CHOICES = "300"
|
||||||
|
const val MOVED_PERMANENTLY = "301"
|
||||||
|
const val FOUND = "302"
|
||||||
|
const val SEE_OTHER = "303"
|
||||||
|
const val NOT_MODIFIED = "304"
|
||||||
|
const val USE_PROXY = "305"
|
||||||
|
const val TEMPORARY_REDIRECT = "307"
|
||||||
|
const val PERMANENT_REDIRECT = "308"
|
||||||
|
const val BAD_REQUEST = "400"
|
||||||
|
const val UNAUTHORIZED = "401"
|
||||||
|
const val PAYMENT_REQUIRED = "402"
|
||||||
|
const val FORBIDDEN = "403"
|
||||||
|
const val NOT_FOUND = "404"
|
||||||
|
const val METHOD_NOT_ALLOWED = "405"
|
||||||
|
const val NOT_ACCEPTABLE = "406"
|
||||||
|
const val PROXY_AUTHENTICATION_REQUIRED = "407"
|
||||||
|
const val REQUEST_TIMEOUT = "408"
|
||||||
|
const val CONFLICT = "409"
|
||||||
|
const val GONE = "410"
|
||||||
|
const val LENGTH_REQUIRED = "411"
|
||||||
|
const val PRECONDITION_FAILED = "412"
|
||||||
|
const val PAYLOAD_TOO_LARGE = "413"
|
||||||
|
const val URI_TOO_LONG = "414"
|
||||||
|
const val UNSUPPORTED_MEDIA_TYPE = "415"
|
||||||
|
const val RANGE_NOT_SATISFIABLE = "416"
|
||||||
|
const val EXPECTATION_FAILED = "417"
|
||||||
|
const val IM_A_TEAPOT = "418"
|
||||||
|
const val MISDIRECTED_REQUEST = "421"
|
||||||
|
const val UNPROCESSABLE_ENTITY = "422"
|
||||||
|
const val LOCKED = "423"
|
||||||
|
const val FAILED_DEPENDENCY = "424"
|
||||||
|
const val TOO_EARLY = "425"
|
||||||
|
const val UPGRADE_REQUIRED = "426"
|
||||||
|
const val PRECONDITION_REQUIRED = "428"
|
||||||
|
const val TOO_MANY_REQUESTS = "429"
|
||||||
|
const val REQUEST_HEADER_FIELDS_TOO_LARGE = "431"
|
||||||
|
const val UNAVAILABLE_FOR_LEGAL_REASONS = "451"
|
||||||
|
const val INTERNAL_SERVER_ERROR = "500"
|
||||||
|
const val NOT_IMPLEMENTED = "501"
|
||||||
|
const val BAD_GATEWAY = "502"
|
||||||
|
const val SERVICE_UNAVAILABLE = "503"
|
||||||
|
const val GATEWAY_TIMEOUT = "504"
|
||||||
|
const val HTTP_VERSION_NOT_SUPPORTED = "505"
|
||||||
|
const val VARIANT_ALSO_NEGOTIATES = "506"
|
||||||
|
const val INSUFFICIENT_STORAGE = "507"
|
||||||
|
const val LOOP_DETECTED = "508"
|
||||||
|
const val NOT_EXTENDED = "510"
|
||||||
|
const val NETWORK_AUTHENTICATION_REQUIRED = "511"
|
||||||
|
}
|
||||||
@@ -5,10 +5,9 @@ import io.visus.demos.kotlinapi.domain.model.ComponentHealth
|
|||||||
import tech.mappie.api.ObjectMappie
|
import tech.mappie.api.ObjectMappie
|
||||||
|
|
||||||
object ComponentHealthDtoMapper : ObjectMappie<ComponentHealth, ComponentHealthDto>() {
|
object ComponentHealthDtoMapper : ObjectMappie<ComponentHealth, ComponentHealthDto>() {
|
||||||
override fun map(from: ComponentHealth): ComponentHealthDto {
|
override fun map(from: ComponentHealth): ComponentHealthDto =
|
||||||
return ComponentHealthDto(
|
ComponentHealthDto(
|
||||||
status = from.status.name,
|
status = from.status.name,
|
||||||
message = from.message,
|
message = from.message,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import io.visus.demos.kotlinapi.domain.model.HealthStatus
|
|||||||
import tech.mappie.api.ObjectMappie
|
import tech.mappie.api.ObjectMappie
|
||||||
|
|
||||||
object HealthResponseMapper : ObjectMappie<HealthStatus, HealthResponse>() {
|
object HealthResponseMapper : ObjectMappie<HealthStatus, HealthResponse>() {
|
||||||
override fun map(from: HealthStatus): HealthResponse {
|
override fun map(from: HealthStatus): HealthResponse =
|
||||||
return HealthResponse(
|
HealthResponse(
|
||||||
status = from.status.name,
|
status = from.status.name,
|
||||||
timestamp = from.timestamp.toString(),
|
timestamp = from.timestamp.toString(),
|
||||||
components = from.components.mapValues { (_, componentHealth) ->
|
components =
|
||||||
|
from.components.mapValues { (_, componentHealth) ->
|
||||||
ComponentHealthDtoMapper.map(componentHealth)
|
ComponentHealthDtoMapper.map(componentHealth)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package io.visus.demos.kotlinapi.config
|
||||||
|
|
||||||
|
import io.visus.demos.kotlinapi.api.ApiVersion
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.core.annotation.AnnotatedElementUtils
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class ApiVersionConfig : WebMvcConfigurer {
|
||||||
|
override fun configurePathMatch(configurer: PathMatchConfigurer) {
|
||||||
|
configurer.addPathPrefix("/api/v1") { controller ->
|
||||||
|
val apiVersion = AnnotatedElementUtils.findMergedAnnotation(controller, ApiVersion::class.java)
|
||||||
|
controller.isAnnotationPresent(RestController::class.java) && apiVersion?.version == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,15 @@ import com.mongodb.client.MongoClients
|
|||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.data.mongodb.config.EnableMongoAuditing
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@EnableMongoAuditing
|
||||||
class MongoConfig {
|
class MongoConfig {
|
||||||
@Bean
|
@Bean
|
||||||
fun mongoClient(
|
fun mongoClient(
|
||||||
@Value("\${spring.mongodb.uri}") uri: String,
|
@Value($$"${spring.mongodb.uri}") uri: String,
|
||||||
): MongoClient {
|
): MongoClient {
|
||||||
val connectionString = ConnectionString(uri)
|
val connectionString = ConnectionString(uri)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package io.visus.demos.kotlinapi.config
|
package io.visus.demos.kotlinapi.config
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.Components
|
||||||
import io.swagger.v3.oas.models.OpenAPI
|
import io.swagger.v3.oas.models.OpenAPI
|
||||||
import io.swagger.v3.oas.models.info.Info
|
import io.swagger.v3.oas.models.info.Info
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityRequirement
|
||||||
|
import io.swagger.v3.oas.models.security.SecurityScheme
|
||||||
|
import org.springdoc.core.models.GroupedOpenApi
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
@@ -13,7 +17,25 @@ class OpenApiConfig {
|
|||||||
.info(
|
.info(
|
||||||
Info()
|
Info()
|
||||||
.title("Kotlin API")
|
.title("Kotlin API")
|
||||||
.version("0.0.1-SNAPSHOT")
|
.version("1.0.0")
|
||||||
.description("API documentation for Kotlin Spring Boot application"),
|
.description("API documentation for Kotlin Spring Boot application"),
|
||||||
)
|
).components(
|
||||||
|
Components()
|
||||||
|
.addSecuritySchemes(
|
||||||
|
"bearerAuth",
|
||||||
|
SecurityScheme()
|
||||||
|
.type(SecurityScheme.Type.HTTP)
|
||||||
|
.scheme("bearer")
|
||||||
|
.bearerFormat("JWT")
|
||||||
|
.description("Enter JWT Bearer token"),
|
||||||
|
),
|
||||||
|
).addSecurityItem(SecurityRequirement().addList("bearerAuth"))
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun apiV1(): GroupedOpenApi =
|
||||||
|
GroupedOpenApi
|
||||||
|
.builder()
|
||||||
|
.group("v1")
|
||||||
|
.pathsToMatch("/api/v1/**")
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,52 @@ package io.visus.demos.kotlinapi.config
|
|||||||
|
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password4j.Argon2Password4jPasswordEncoder
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
import org.springframework.web.cors.CorsConfiguration
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity
|
||||||
class SecurityConfig {
|
class SecurityConfig {
|
||||||
|
@Bean
|
||||||
|
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||||
|
val configuration =
|
||||||
|
CorsConfiguration().apply {
|
||||||
|
allowedOriginPatterns = listOf("*")
|
||||||
|
allowedMethods = listOf("*")
|
||||||
|
allowedHeaders = listOf("*")
|
||||||
|
allowCredentials = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return UrlBasedCorsConfigurationSource().apply {
|
||||||
|
registerCorsConfiguration("/**", configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun passwordEncoder(): PasswordEncoder = Argon2Password4jPasswordEncoder()
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
http
|
http
|
||||||
|
.csrf { it.disable() }
|
||||||
|
.cors { it.configurationSource(corsConfigurationSource()) }
|
||||||
.authorizeHttpRequests { authorize ->
|
.authorizeHttpRequests { authorize ->
|
||||||
authorize
|
authorize
|
||||||
.requestMatchers("/health")
|
.requestMatchers(
|
||||||
.permitAll()
|
"/swagger-ui.html",
|
||||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**")
|
"/swagger-ui/**",
|
||||||
.permitAll()
|
"/v3/api-docs/**",
|
||||||
|
"/api-docs/**",
|
||||||
|
"/api/v1/health",
|
||||||
|
).permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated()
|
.authenticated()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,26 @@ import io.swagger.v3.oas.annotations.Operation
|
|||||||
import io.swagger.v3.oas.annotations.media.Content
|
import io.swagger.v3.oas.annotations.media.Content
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirements
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag
|
import io.swagger.v3.oas.annotations.tags.Tag
|
||||||
|
import io.visus.demos.kotlinapi.api.ApiVersion
|
||||||
|
import io.visus.demos.kotlinapi.api.HttpStatusCodes
|
||||||
import io.visus.demos.kotlinapi.api.dto.HealthResponse
|
import io.visus.demos.kotlinapi.api.dto.HealthResponse
|
||||||
|
import io.visus.demos.kotlinapi.api.mappers.HealthResponseMapper
|
||||||
|
import io.visus.demos.kotlinapi.domain.model.Status
|
||||||
import io.visus.demos.kotlinapi.domain.service.HealthService
|
import io.visus.demos.kotlinapi.domain.service.HealthService
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@ApiVersion(version = 1)
|
||||||
|
@PreAuthorize("permitAll()")
|
||||||
|
@SecurityRequirements(SecurityRequirement(name = "bearerAuth"))
|
||||||
@Tag(name = "Health", description = "Health check endpoints")
|
@Tag(name = "Health", description = "Health check endpoints")
|
||||||
class HealthController(
|
class HealthController(
|
||||||
private val healthCheckService: HealthService,
|
private val healthCheckService: HealthService,
|
||||||
@@ -23,7 +34,7 @@ class HealthController(
|
|||||||
description = "Returns the health status of the API",
|
description = "Returns the health status of the API",
|
||||||
responses = [
|
responses = [
|
||||||
ApiResponse(
|
ApiResponse(
|
||||||
responseCode = "200",
|
responseCode = HttpStatusCodes.OK,
|
||||||
description = "API is healthy",
|
description = "API is healthy",
|
||||||
content = [
|
content = [
|
||||||
Content(
|
Content(
|
||||||
@@ -33,7 +44,7 @@ class HealthController(
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
ApiResponse(
|
ApiResponse(
|
||||||
responseCode = "503",
|
responseCode = HttpStatusCodes.SERVICE_UNAVAILABLE,
|
||||||
description = "API is down or degraded",
|
description = "API is down or degraded",
|
||||||
content = [
|
content = [
|
||||||
Content(
|
Content(
|
||||||
@@ -44,5 +55,16 @@ class HealthController(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
fun health(): ResponseEntity<HealthResponse> = healthCheckService.check()
|
fun health(): ResponseEntity<HealthResponse> {
|
||||||
|
val result = healthCheckService.check()
|
||||||
|
val response = HealthResponseMapper.map(result)
|
||||||
|
|
||||||
|
val httpStatus =
|
||||||
|
when (result.status) {
|
||||||
|
Status.UP -> HttpStatus.OK
|
||||||
|
Status.DOWN, Status.DEGRADED -> HttpStatus.SERVICE_UNAVAILABLE
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(httpStatus).body(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package io.visus.demos.kotlinapi.domain.port
|
package io.visus.demos.kotlinapi.domain
|
||||||
|
|
||||||
import io.visus.demos.kotlinapi.domain.model.ComponentHealth
|
import io.visus.demos.kotlinapi.domain.model.ComponentHealth
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package io.visus.demos.kotlinapi.domain.model
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.CreatedDate
|
||||||
|
import org.springframework.data.annotation.Id
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate
|
||||||
|
import org.springframework.data.mongodb.core.index.Indexed
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Field
|
||||||
|
import org.springframework.security.core.GrantedAuthority
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Document(collection = "users")
|
||||||
|
data class User(
|
||||||
|
@Id
|
||||||
|
val id: String? = null,
|
||||||
|
@Indexed(unique = true)
|
||||||
|
val email: String,
|
||||||
|
@Field("password")
|
||||||
|
private val passwordHash: String,
|
||||||
|
@Indexed
|
||||||
|
val role: String,
|
||||||
|
@CreatedDate
|
||||||
|
val createdAt: Instant? = null,
|
||||||
|
@LastModifiedDate
|
||||||
|
val updatedAt: Instant? = null,
|
||||||
|
) : UserDetails {
|
||||||
|
override fun getAuthorities(): Collection<GrantedAuthority> = listOf(SimpleGrantedAuthority(role))
|
||||||
|
|
||||||
|
override fun getPassword(): String = passwordHash
|
||||||
|
|
||||||
|
override fun getUsername(): String = email
|
||||||
|
|
||||||
|
override fun isAccountNonExpired() = true
|
||||||
|
|
||||||
|
override fun isAccountNonLocked() = true
|
||||||
|
|
||||||
|
override fun isCredentialsNonExpired() = true
|
||||||
|
|
||||||
|
override fun isEnabled() = true
|
||||||
|
}
|
||||||
@@ -1,29 +1,15 @@
|
|||||||
package io.visus.demos.kotlinapi.domain.service
|
package io.visus.demos.kotlinapi.domain.service
|
||||||
|
|
||||||
import io.visus.demos.kotlinapi.api.dto.HealthResponse
|
import io.visus.demos.kotlinapi.domain.HealthIndicator
|
||||||
import io.visus.demos.kotlinapi.api.mappers.HealthResponseMapper
|
|
||||||
import io.visus.demos.kotlinapi.domain.port.HealthIndicator
|
|
||||||
import io.visus.demos.kotlinapi.domain.model.HealthStatus
|
import io.visus.demos.kotlinapi.domain.model.HealthStatus
|
||||||
import io.visus.demos.kotlinapi.domain.model.Status
|
|
||||||
import org.springframework.http.HttpStatus
|
|
||||||
import org.springframework.http.ResponseEntity
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class HealthService(
|
class HealthService(
|
||||||
private val healthIndicators: List<HealthIndicator>,
|
private val healthIndicators: List<HealthIndicator>,
|
||||||
) {
|
) {
|
||||||
fun check(): ResponseEntity<HealthResponse> {
|
fun check(): HealthStatus {
|
||||||
val componentHealths = healthIndicators.map { it.check() }
|
val componentHealths = healthIndicators.map { it.check() }
|
||||||
val healthStatus = HealthStatus.fromComponents(componentHealths)
|
return HealthStatus.fromComponents(componentHealths)
|
||||||
val response = HealthResponseMapper.map(healthStatus)
|
|
||||||
|
|
||||||
val httpStatus =
|
|
||||||
when (healthStatus.status) {
|
|
||||||
Status.UP -> HttpStatus.OK
|
|
||||||
Status.DOWN, Status.DEGRADED -> HttpStatus.SERVICE_UNAVAILABLE
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.status(httpStatus).body(response)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package io.visus.demos.kotlinapi.domain.service
|
||||||
|
|
||||||
|
import io.visus.demos.kotlinapi.infrastructure.user.UserRepository
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class UserService(
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
) : UserDetailsService {
|
||||||
|
override fun loadUserByUsername(username: String): UserDetails {
|
||||||
|
if (username.isBlank()) throw UsernameNotFoundException("Username must not be null or empty")
|
||||||
|
|
||||||
|
return userRepository.findByEmail(username)
|
||||||
|
?: throw UsernameNotFoundException("User not found with email: $username")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package io.visus.demos.kotlinapi.infrastructure.health
|
package io.visus.demos.kotlinapi.infrastructure.health
|
||||||
|
|
||||||
import com.mongodb.client.MongoClient
|
import com.mongodb.client.MongoClient
|
||||||
|
import io.visus.demos.kotlinapi.domain.HealthIndicator
|
||||||
import io.visus.demos.kotlinapi.domain.model.ComponentHealth
|
import io.visus.demos.kotlinapi.domain.model.ComponentHealth
|
||||||
import io.visus.demos.kotlinapi.domain.model.ComponentStatus
|
import io.visus.demos.kotlinapi.domain.model.ComponentStatus
|
||||||
import io.visus.demos.kotlinapi.domain.port.HealthIndicator
|
|
||||||
import org.bson.Document
|
import org.bson.Document
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package io.visus.demos.kotlinapi.infrastructure.user
|
||||||
|
|
||||||
|
import io.visus.demos.kotlinapi.domain.model.User
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface UserRepository : MongoRepository<User, String> {
|
||||||
|
fun findByEmail(email: String): User?
|
||||||
|
}
|
||||||
54
src/main/kotlin/io/visus/demos/kotlinapi/seed/DataSeeder.kt
Normal file
54
src/main/kotlin/io/visus/demos/kotlinapi/seed/DataSeeder.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package io.visus.demos.kotlinapi.seed
|
||||||
|
|
||||||
|
import io.visus.demos.kotlinapi.domain.model.User
|
||||||
|
import io.visus.demos.kotlinapi.infrastructure.user.UserRepository
|
||||||
|
import org.springframework.boot.ApplicationArguments
|
||||||
|
import org.springframework.boot.ApplicationRunner
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class DataSeeder(
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val passwordEncoder: PasswordEncoder,
|
||||||
|
) : ApplicationRunner {
|
||||||
|
override fun run(args: ApplicationArguments) {
|
||||||
|
seedUser(
|
||||||
|
email = "user@example.com",
|
||||||
|
rawPassword = "user12345",
|
||||||
|
role = "ROLE_USER",
|
||||||
|
)
|
||||||
|
|
||||||
|
seedUser(
|
||||||
|
email = "admin@example.com",
|
||||||
|
rawPassword = "admin12345",
|
||||||
|
role = "ROLE_ADMIN",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun seedUser(
|
||||||
|
email: String,
|
||||||
|
rawPassword: String,
|
||||||
|
role: String,
|
||||||
|
) {
|
||||||
|
if (userRepository.findByEmail(email) != null) {
|
||||||
|
println("User '$email' already exists — skipping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val hashedPassword =
|
||||||
|
passwordEncoder.encode(rawPassword)
|
||||||
|
?: error("Password encoding returned null")
|
||||||
|
|
||||||
|
val user =
|
||||||
|
User(
|
||||||
|
email = email,
|
||||||
|
passwordHash = hashedPassword,
|
||||||
|
role = role,
|
||||||
|
)
|
||||||
|
|
||||||
|
val saved = userRepository.save(user)
|
||||||
|
val savedId = saved.id ?: "unknown"
|
||||||
|
println("Seeded user '${saved.email}' with id=$savedId and role=${saved.role}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: kotlin-api
|
name: kotlin-api
|
||||||
|
data:
|
||||||
|
mongodb:
|
||||||
|
auto-index-creation: true
|
||||||
mongodb:
|
mongodb:
|
||||||
uri: ${MONGODB_URI:mongodb://localhost:27017/kotlin-api}
|
uri: ${MONGODB_URI:mongodb://localhost:27017/kotlin-api}
|
||||||
|
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
path: /api-docs
|
||||||
|
swagger-ui:
|
||||||
|
path: /swagger-ui.html
|
||||||
|
|||||||
Reference in New Issue
Block a user