#68 Ajout des configurations "secure" et réécriture de la plupart du système de configuration
This commit is contained in:
parent
d58e703473
commit
febef06962
|
@ -83,8 +83,8 @@ sourceSets {
|
|||
|
||||
tasks.test {
|
||||
reports {
|
||||
junitXml.isEnabled = true
|
||||
html.isEnabled = false
|
||||
junitXml.required.set(true)
|
||||
html.required.set(false)
|
||||
}
|
||||
|
||||
useJUnitPlatform()
|
||||
|
@ -99,7 +99,6 @@ tasks.withType<JavaCompile>() {
|
|||
tasks.withType<KotlinCompile>().all {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
useIR = true
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-Xinline-classes"
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
version: "3.1"
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: fyloz.dev:5443/color-recipes-explorer/frontend:latest
|
||||
ports:
|
||||
- 4200:80
|
||||
database:
|
||||
image: mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: "pass"
|
||||
MYSQL_DATABASE: "cre"
|
||||
ports:
|
||||
- 3306:3306
|
||||
|
|
@ -1,20 +1,17 @@
|
|||
package dev.fyloz.colorrecipesexplorer
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.FileConfiguration
|
||||
import dev.fyloz.colorrecipesexplorer.databasemanager.CreDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.databasemanager.CreDatabaseException
|
||||
import dev.fyloz.colorrecipesexplorer.databasemanager.databaseContext
|
||||
import dev.fyloz.colorrecipesexplorer.databasemanager.databaseUpdaterProperties
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
||||
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.DependsOn
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.core.env.ConfigurableEnvironment
|
||||
import java.lang.RuntimeException
|
||||
import javax.sql.DataSource
|
||||
|
||||
const val SUPPORTED_DATABASE_VERSION = 5
|
||||
|
@ -23,21 +20,20 @@ val DATABASE_NAME_REGEX = Regex("(\\w+)$")
|
|||
|
||||
@Profile("!emergency")
|
||||
@Configuration
|
||||
@DependsOn("configurationsInitializer")
|
||||
@DependsOn("configurationsInitializer", "configurationService")
|
||||
class DataSourceConfiguration {
|
||||
@Bean(name = ["dataSource"])
|
||||
fun customDataSource(
|
||||
logger: Logger,
|
||||
environment: ConfigurableEnvironment,
|
||||
fileConfiguration: FileConfiguration,
|
||||
databaseUpdaterProperties: DatabaseUpdaterProperties
|
||||
configurationService: ConfigurationService
|
||||
): DataSource {
|
||||
fun getConfiguration(type: ConfigurationType, defaultProperty: String) =
|
||||
fileConfiguration.get(type)?.content ?: defaultProperty
|
||||
fun getConfiguration(type: ConfigurationType) =
|
||||
configurationService.get(type).content
|
||||
|
||||
val databaseUrl = "jdbc:" + getConfiguration(ConfigurationType.DATABASE_URL, databaseUpdaterProperties.url)
|
||||
val databaseUsername = getConfiguration(ConfigurationType.DATABASE_USER, databaseUpdaterProperties.username)
|
||||
val databasePassword = getConfiguration(ConfigurationType.DATABASE_PASSWORD, databaseUpdaterProperties.password)
|
||||
val databaseUrl = "jdbc:" + getConfiguration(ConfigurationType.DATABASE_URL)
|
||||
val databaseUsername = getConfiguration(ConfigurationType.DATABASE_USER)
|
||||
val databasePassword = getConfiguration(ConfigurationType.DATABASE_PASSWORD)
|
||||
|
||||
try {
|
||||
runDatabaseVersionCheck(logger, databaseUrl, DatabaseUpdaterProperties().apply {
|
||||
|
@ -158,7 +154,6 @@ fun throwUnsupportedDatabaseVersion(version: Int, logger: Logger) {
|
|||
throw DatabaseVersioningException.UnsupportedDatabaseVersion(version)
|
||||
}
|
||||
|
||||
@ConfigurationProperties(prefix = "cre.database")
|
||||
class DatabaseUpdaterProperties {
|
||||
var url: String = ""
|
||||
var username: String = ""
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
||||
import dev.fyloz.colorrecipesexplorer.restartApplication
|
||||
import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.ApplicationListener
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.core.annotation.Order
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
@Configuration
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@Profile("!emergency")
|
||||
class ApplicationReadyListener(
|
||||
private val materialTypeService: MaterialTypeService,
|
||||
private val materialTypeProperties: MaterialTypeProperties,
|
||||
private val creProperties: CreProperties,
|
||||
private val logger: Logger
|
||||
) : ApplicationListener<ApplicationReadyEvent> {
|
||||
override fun onApplicationEvent(event: ApplicationReadyEvent) {
|
||||
if (emergencyMode) {
|
||||
logger.error("Emergency mode is enabled, default material types will not be created")
|
||||
thread {
|
||||
Thread.sleep(1000)
|
||||
logger.warn("Restarting in emergency mode...")
|
||||
restartApplication(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
materialTypeService.saveSystemTypes(materialTypeProperties.systemTypes)
|
||||
CRE_PROPERTIES = creProperties
|
||||
}
|
||||
}
|
||||
|
||||
class ApplicationInitializer : ApplicationListener<ApplicationEnvironmentPreparedEvent> {
|
||||
override fun onApplicationEvent(event: ApplicationEnvironmentPreparedEvent) {
|
||||
if (emergencyMode) {
|
||||
event.environment.setActiveProfiles("emergency")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
||||
import dev.fyloz.colorrecipesexplorer.model.configuration
|
||||
import dev.fyloz.colorrecipesexplorer.service.create
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
const val CONFIGURATION_FILE_PATH = "config.properties"
|
||||
const val CONFIGURATION_FILE_COMMENT = "---Color Recipes Explorer configuration---"
|
||||
|
||||
@Configuration
|
||||
class ConfigurationsInitializer(
|
||||
private val creProperties: CreProperties
|
||||
) {
|
||||
@Bean
|
||||
fun fileConfiguration() = FileConfiguration("${creProperties.configDirectory}/$CONFIGURATION_FILE_PATH")
|
||||
}
|
||||
|
||||
class FileConfiguration(private val configFilePath: String) {
|
||||
val properties = Properties().apply {
|
||||
with(File(configFilePath)) {
|
||||
if (!this.exists()) this.create()
|
||||
FileInputStream(this).use {
|
||||
this@apply.load(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun get(type: ConfigurationType) =
|
||||
if (properties.containsKey(type.key))
|
||||
configuration(
|
||||
type,
|
||||
properties[type.key] as String,
|
||||
LocalDateTime.parse(properties[configurationLastUpdateKey(type.key)] as String)
|
||||
)
|
||||
else null
|
||||
|
||||
fun set(type: ConfigurationType, content: String) {
|
||||
properties[type.key] = content
|
||||
properties[configurationLastUpdateKey(type.key)] = LocalDateTime.now().toString()
|
||||
save()
|
||||
}
|
||||
|
||||
fun save() {
|
||||
FileOutputStream(configFilePath).use {
|
||||
properties.store(it, CONFIGURATION_FILE_COMMENT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configurationLastUpdateKey(key: String) = "$key.last-updated"
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
import org.springframework.security.core.userdetails.User as SpringUser
|
||||
|
||||
@Configuration
|
||||
@Profile("emergency")
|
||||
@EnableConfigurationProperties(SecurityConfigurationProperties::class)
|
||||
class EmergencySecurityConfig(
|
||||
val securityConfigurationProperties: SecurityConfigurationProperties,
|
||||
val environment: Environment
|
||||
) : WebSecurityConfigurerAdapter() {
|
||||
init {
|
||||
emergencyMode = true
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun corsConfigurationSource() =
|
||||
UrlBasedCorsConfigurationSource().apply {
|
||||
registerCorsConfiguration("/**", CorsConfiguration().apply {
|
||||
allowedOrigins = listOf("http://localhost:4200") // Angular development server
|
||||
allowedMethods = listOf(
|
||||
HttpMethod.GET.name,
|
||||
HttpMethod.POST.name,
|
||||
HttpMethod.PUT.name,
|
||||
HttpMethod.DELETE.name,
|
||||
HttpMethod.OPTIONS.name,
|
||||
HttpMethod.HEAD.name
|
||||
)
|
||||
allowCredentials = true
|
||||
}.applyPermitDefaultValues())
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder() =
|
||||
BCryptPasswordEncoder()
|
||||
|
||||
override fun configure(auth: AuthenticationManagerBuilder) {
|
||||
auth.inMemoryAuthentication()
|
||||
.withUser(securityConfigurationProperties.root!!.id.toString())
|
||||
.password(passwordEncoder().encode(securityConfigurationProperties.root!!.password))
|
||||
.authorities(SimpleGrantedAuthority("ADMIN"))
|
||||
}
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
val debugMode = "debug" in environment.activeProfiles
|
||||
|
||||
http
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.addFilter(
|
||||
JwtAuthenticationFilter(
|
||||
authenticationManager(),
|
||||
securityConfigurationProperties
|
||||
) { }
|
||||
)
|
||||
.addFilter(
|
||||
JwtAuthorizationFilter(
|
||||
securityConfigurationProperties,
|
||||
authenticationManager(),
|
||||
this::loadUserById
|
||||
)
|
||||
)
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("**").permitAll()
|
||||
|
||||
if (debugMode) {
|
||||
http
|
||||
.cors()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUserById(id: Long): UserDetails {
|
||||
if (id == securityConfigurationProperties.root!!.id) {
|
||||
return SpringUser(
|
||||
id.toString(),
|
||||
securityConfigurationProperties.root!!.password,
|
||||
listOf(SimpleGrantedAuthority("ADMIN"))
|
||||
)
|
||||
}
|
||||
throw UsernameNotFoundException(id.toString())
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import org.springframework.context.annotation.Bean
|
|||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(MaterialTypeProperties::class, CreProperties::class, DatabaseUpdaterProperties::class)
|
||||
@EnableConfigurationProperties(MaterialTypeProperties::class, CreProperties::class)
|
||||
class SpringConfiguration {
|
||||
@Bean
|
||||
fun logger(): Logger = LoggerFactory.getLogger(ColorRecipesExplorerApplication::class.java)
|
||||
|
|
|
@ -1,293 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest
|
||||
import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsService
|
||||
import dev.fyloz.colorrecipesexplorer.service.UserService
|
||||
import io.jsonwebtoken.ExpiredJwtException
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.SignatureAlgorithm
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
|
||||
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.WebSecurityConfigurerAdapter
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.util.Assert
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
import org.springframework.web.util.WebUtils
|
||||
import java.util.*
|
||||
import javax.annotation.PostConstruct
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import org.springframework.security.core.userdetails.User as SpringUser
|
||||
|
||||
@Configuration
|
||||
@Profile("!emergency")
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
@EnableConfigurationProperties(SecurityConfigurationProperties::class)
|
||||
class WebSecurityConfig(
|
||||
val securityConfigurationProperties: SecurityConfigurationProperties,
|
||||
@Lazy val userDetailsService: CreUserDetailsService,
|
||||
@Lazy val userService: UserService,
|
||||
val environment: Environment,
|
||||
val logger: Logger
|
||||
) : WebSecurityConfigurerAdapter() {
|
||||
var debugMode = false
|
||||
|
||||
override fun configure(authBuilder: AuthenticationManagerBuilder) {
|
||||
authBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder())
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder() =
|
||||
BCryptPasswordEncoder()
|
||||
|
||||
@Bean
|
||||
override fun authenticationManagerBean(): AuthenticationManager =
|
||||
super.authenticationManagerBean()
|
||||
|
||||
@Bean
|
||||
fun corsConfigurationSource() =
|
||||
UrlBasedCorsConfigurationSource().apply {
|
||||
registerCorsConfiguration("/**", CorsConfiguration().apply {
|
||||
allowedOrigins = listOf("http://localhost:4200") // Angular development server
|
||||
allowedMethods = listOf(
|
||||
HttpMethod.GET.name,
|
||||
HttpMethod.POST.name,
|
||||
HttpMethod.PUT.name,
|
||||
HttpMethod.DELETE.name,
|
||||
HttpMethod.OPTIONS.name,
|
||||
HttpMethod.HEAD.name
|
||||
)
|
||||
allowCredentials = true
|
||||
}.applyPermitDefaultValues())
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
fun initWebSecurity() {
|
||||
fun createUser(
|
||||
credentials: SecurityConfigurationProperties.SystemUserCredentials?,
|
||||
firstName: String,
|
||||
lastName: String,
|
||||
permissions: List<Permission>
|
||||
) {
|
||||
if (emergencyMode) {
|
||||
logger.error("Emergency mode is enabled, root user will not be created")
|
||||
return
|
||||
}
|
||||
|
||||
Assert.notNull(credentials, "No root user has been defined.")
|
||||
credentials!!
|
||||
Assert.notNull(credentials.id, "The root user has no identifier defined.")
|
||||
Assert.notNull(credentials.password, "The root user has no password defined.")
|
||||
if (!userService.existsById(credentials.id!!)) {
|
||||
userService.save(
|
||||
User(
|
||||
id = credentials.id!!,
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
password = passwordEncoder().encode(credentials.password!!),
|
||||
isSystemUser = true,
|
||||
permissions = permissions.toMutableSet()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
createUser(securityConfigurationProperties.root, "Root", "User", listOf(Permission.ADMIN))
|
||||
debugMode = "debug" in environment.activeProfiles
|
||||
if (debugMode) logger.warn("Debug mode is enabled, security will be disabled!")
|
||||
}
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
.csrf().disable()
|
||||
.addFilter(
|
||||
JwtAuthenticationFilter(
|
||||
authenticationManager(),
|
||||
securityConfigurationProperties
|
||||
) { userService.updateLastLoginTime(it) }
|
||||
)
|
||||
.addFilter(
|
||||
JwtAuthorizationFilter(
|
||||
securityConfigurationProperties,
|
||||
authenticationManager()
|
||||
) { userDetailsService.loadUserById(it, false) }
|
||||
)
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
|
||||
if (!debugMode) {
|
||||
http.authorizeRequests()
|
||||
.antMatchers("/api/login").permitAll()
|
||||
.antMatchers("/api/logout").authenticated()
|
||||
.antMatchers("/api/user/current").authenticated()
|
||||
.anyRequest().authenticated()
|
||||
} else {
|
||||
http
|
||||
.cors()
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("**").permitAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
class RestAuthenticationEntryPoint : AuthenticationEntryPoint {
|
||||
override fun commence(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authException: AuthenticationException
|
||||
) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
|
||||
}
|
||||
|
||||
const val authorizationCookieName = "Authorization"
|
||||
const val defaultGroupCookieName = "Default-Group"
|
||||
val blacklistedJwtTokens = mutableListOf<String>()
|
||||
|
||||
class JwtAuthenticationFilter(
|
||||
private val authManager: AuthenticationManager,
|
||||
private val securityConfigurationProperties: SecurityConfigurationProperties,
|
||||
private val updateUserLoginTime: (Long) -> Unit
|
||||
) : UsernamePasswordAuthenticationFilter() {
|
||||
private var debugMode = false
|
||||
|
||||
init {
|
||||
setFilterProcessesUrl("/api/login")
|
||||
debugMode = "debug" in environment.activeProfiles
|
||||
}
|
||||
|
||||
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
|
||||
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequest::class.java)
|
||||
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
|
||||
}
|
||||
|
||||
override fun successfulAuthentication(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
chain: FilterChain,
|
||||
authResult: Authentication
|
||||
) {
|
||||
val jwtSecret = securityConfigurationProperties.jwtSecret
|
||||
val jwtDuration = securityConfigurationProperties.jwtDuration
|
||||
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
|
||||
Assert.notNull(jwtDuration, "No JWT duration has been defined.")
|
||||
val userId = (authResult.principal as SpringUser).username
|
||||
updateUserLoginTime(userId.toLong())
|
||||
val expirationMs = System.currentTimeMillis() + jwtDuration!!
|
||||
val expirationDate = Date(expirationMs)
|
||||
val token = Jwts.builder()
|
||||
.setSubject(userId)
|
||||
.setExpiration(expirationDate)
|
||||
.signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
|
||||
.compact()
|
||||
response.addHeader("Access-Control-Expose-Headers", "X-Authentication-Expiration")
|
||||
var bearerCookie =
|
||||
"$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; SameSite=strict"
|
||||
if (!debugMode) bearerCookie += "; Secure;"
|
||||
response.addHeader(
|
||||
"Set-Cookie",
|
||||
bearerCookie
|
||||
)
|
||||
response.addHeader(authorizationCookieName, "Bearer $token")
|
||||
response.addHeader("X-Authentication-Expiration", "$expirationMs")
|
||||
}
|
||||
}
|
||||
|
||||
class JwtAuthorizationFilter(
|
||||
private val securityConfigurationProperties: SecurityConfigurationProperties,
|
||||
authenticationManager: AuthenticationManager,
|
||||
private val loadUserById: (Long) -> UserDetails
|
||||
) : BasicAuthenticationFilter(authenticationManager) {
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
|
||||
fun tryLoginFromBearer(): Boolean {
|
||||
val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
|
||||
// Check for an authorization token cookie or header
|
||||
val authorizationToken = if (authorizationCookie != null)
|
||||
authorizationCookie.value
|
||||
else
|
||||
request.getHeader(authorizationCookieName)
|
||||
|
||||
// An authorization token is valid if it starts with "Bearer", is not expired and is not blacklisted
|
||||
if (authorizationToken != null && authorizationToken.startsWith("Bearer") && authorizationToken !in blacklistedJwtTokens) {
|
||||
val authenticationToken = getAuthentication(authorizationToken) ?: return false
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun tryLoginFromDefaultGroupCookie() {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
if (defaultGroupCookie != null) {
|
||||
val authenticationToken = getAuthenticationToken(defaultGroupCookie.value)
|
||||
SecurityContextHolder.getContext().authentication = authenticationToken
|
||||
}
|
||||
}
|
||||
|
||||
if (!tryLoginFromBearer())
|
||||
tryLoginFromDefaultGroupCookie()
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
|
||||
private fun getAuthentication(token: String): UsernamePasswordAuthenticationToken? {
|
||||
val jwtSecret = securityConfigurationProperties.jwtSecret
|
||||
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
|
||||
return try {
|
||||
val userId = Jwts.parser()
|
||||
.setSigningKey(jwtSecret!!.toByteArray())
|
||||
.parseClaimsJws(token.replace("Bearer", ""))
|
||||
.body
|
||||
.subject
|
||||
if (userId != null) getAuthenticationToken(userId) else null
|
||||
} catch (_: ExpiredJwtException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthenticationToken(userId: String): UsernamePasswordAuthenticationToken? = try {
|
||||
val userDetails = loadUserById(userId.toLong())
|
||||
UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities)
|
||||
} catch (_: NotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@ConfigurationProperties("cre.security")
|
||||
class SecurityConfigurationProperties {
|
||||
var jwtSecret: String? = null
|
||||
var jwtDuration: Long? = null
|
||||
var root: SystemUserCredentials? = null
|
||||
|
||||
class SystemUserCredentials(var id: Long? = null, var password: String? = null)
|
||||
}
|
|
@ -1,15 +1,31 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.properties
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import kotlin.properties.Delegates.notNull
|
||||
|
||||
const val DEFAULT_DATA_DIRECTORY = "data"
|
||||
const val DEFAULT_CONFIG_DIRECTORY = "config"
|
||||
const val DEFAULT_DEPLOYMENT_URL = "http://localhost"
|
||||
|
||||
@ConfigurationProperties(prefix = "cre.server")
|
||||
class CreProperties {
|
||||
var dataDirectory: String = DEFAULT_DATA_DIRECTORY
|
||||
var configDirectory: String = DEFAULT_CONFIG_DIRECTORY
|
||||
var deploymentUrl: String = DEFAULT_DEPLOYMENT_URL
|
||||
var cacheGeneratedFiles: Boolean = false
|
||||
}
|
||||
|
||||
@ConfigurationProperties(prefix = "cre.security")
|
||||
class CreSecurityProperties {
|
||||
// JWT
|
||||
var jwtSecret by notNull<String>()
|
||||
var jwtDuration by notNull<Long>()
|
||||
|
||||
// Configs
|
||||
var configSalt: String? = null
|
||||
|
||||
// Users
|
||||
var root: SystemUserCredentials? = null
|
||||
|
||||
class SystemUserCredentials{
|
||||
var id by notNull<Long>()
|
||||
var password by notNull<String>()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import org.springframework.util.Assert
|
|||
@ConfigurationProperties(prefix = "entities.material-types")
|
||||
class MaterialTypeProperties {
|
||||
var systemTypes: MutableList<MaterialTypeProperty> = mutableListOf()
|
||||
var baseName: String = ""
|
||||
|
||||
data class MaterialTypeProperty(
|
||||
var name: String = "",
|
||||
|
|
|
@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.model
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.utils.months
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.time.LocalDateTime
|
||||
|
@ -71,31 +72,35 @@ fun configuration(
|
|||
|
||||
enum class ConfigurationType(
|
||||
val key: String,
|
||||
val defaultContent: Any? = null,
|
||||
val computed: Boolean = false,
|
||||
val file: Boolean = false,
|
||||
val requireRestart: Boolean = false,
|
||||
val public: Boolean = false
|
||||
val public: Boolean = false,
|
||||
val secure: Boolean = false
|
||||
) {
|
||||
INSTANCE_NAME("instance.name", public = true),
|
||||
INSTANCE_LOGO_PATH("instance.logo.path", public = true),
|
||||
INSTANCE_ICON_PATH("instance.icon.path", public = true),
|
||||
INSTANCE_URL("instance.url", public = true),
|
||||
INSTANCE_NAME("instance.name", defaultContent = "Color Recipes Explorer", public = true),
|
||||
INSTANCE_LOGO_PATH("instance.logo.path", defaultContent = "images/logo", public = true),
|
||||
INSTANCE_ICON_PATH("instance.icon.path", defaultContent = "images/icon", public = true),
|
||||
INSTANCE_URL("instance.url", "http://localhost:9090", public = true),
|
||||
|
||||
DATABASE_URL("database.url", file = true, requireRestart = true),
|
||||
DATABASE_USER("database.user", file = true, requireRestart = true),
|
||||
DATABASE_PASSWORD("database.password", file = true, requireRestart = true),
|
||||
DATABASE_URL("database.url", defaultContent = "mysql://localhost/cre", file = true, requireRestart = true),
|
||||
DATABASE_USER("database.user", defaultContent = "cre", file = true, requireRestart = true),
|
||||
DATABASE_PASSWORD("database.password", defaultContent = "asecurepassword", file = true, requireRestart = true, secure = true),
|
||||
DATABASE_SUPPORTED_VERSION("database.version.supported", computed = true),
|
||||
|
||||
RECIPE_APPROBATION_EXPIRATION("recipe.approbation.expiration"),
|
||||
RECIPE_APPROBATION_EXPIRATION("recipe.approbation.expiration", defaultContent = 4.months),
|
||||
|
||||
TOUCH_UP_KIT_CACHE_PDF("touchupkit.pdf.cache"),
|
||||
TOUCH_UP_KIT_EXPIRATION("touchupkit.expiration"),
|
||||
TOUCH_UP_KIT_CACHE_PDF("touchupkit.pdf.cache", defaultContent = true),
|
||||
TOUCH_UP_KIT_EXPIRATION("touchupkit.expiration", defaultContent = 1.months),
|
||||
|
||||
EMERGENCY_MODE_ENABLED("env.emergency", computed = true, public = true),
|
||||
BUILD_VERSION("env.build.version", computed = true),
|
||||
BUILD_TIME("env.build.time", computed = true),
|
||||
JAVA_VERSION("env.java.version", computed = true),
|
||||
OPERATING_SYSTEM("env.os", computed = true)
|
||||
OPERATING_SYSTEM("env.os", computed = true),
|
||||
|
||||
GENERATED_ENCRYPTION_SALT("security.salt", file = true, requireRestart = true)
|
||||
;
|
||||
|
||||
override fun toString() = key
|
||||
|
|
|
@ -6,7 +6,7 @@ import dev.fyloz.colorrecipesexplorer.model.ConfigurationImageDto
|
|||
import dev.fyloz.colorrecipesexplorer.model.account.Permission
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.toAuthority
|
||||
import dev.fyloz.colorrecipesexplorer.restartApplication
|
||||
import dev.fyloz.colorrecipesexplorer.service.ConfigurationService
|
||||
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
@ -21,12 +21,12 @@ class ConfigurationController(val configurationService: ConfigurationService) {
|
|||
ok(with(configurationService) {
|
||||
if (keys != null) getAll(keys) else getAll()
|
||||
}.filter {
|
||||
authentication.hasAuthority(it)
|
||||
!it.type.secure && authentication.hasAuthority(it)
|
||||
})
|
||||
|
||||
@GetMapping("{key}")
|
||||
fun get(@PathVariable key: String, authentication: Authentication?) = with(configurationService.get(key)) {
|
||||
if (authentication.hasAuthority(this)) ok(this) else forbidden()
|
||||
if (!this.type.secure && authentication.hasAuthority(this)) ok(this) else forbidden()
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
||||
import dev.fyloz.colorrecipesexplorer.service.ConfigurationService
|
||||
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
||||
import dev.fyloz.colorrecipesexplorer.service.FileService
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.http.MediaType
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.blacklistedJwtTokens
|
||||
import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.blacklistedJwtTokens
|
||||
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.*
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.or
|
||||
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
|
||||
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
|
||||
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.DatabaseUpdaterProperties
|
||||
import dev.fyloz.colorrecipesexplorer.SUPPORTED_DATABASE_VERSION
|
||||
import dev.fyloz.colorrecipesexplorer.config.FileConfiguration
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.ConfigurationRepository
|
||||
import org.springframework.boot.info.BuildProperties
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
import java.time.ZoneId
|
||||
import javax.annotation.PostConstruct
|
||||
|
||||
interface ConfigurationService {
|
||||
/** Gets all set configurations. */
|
||||
fun getAll(): List<Configuration>
|
||||
|
||||
/**
|
||||
* Gets all configurations with keys contained in the given [formattedKeyList].
|
||||
* The [formattedKeyList] contains wanted configuration keys separated by a semi-colon.
|
||||
*/
|
||||
fun getAll(formattedKeyList: String): List<Configuration>
|
||||
|
||||
/**
|
||||
* Gets the configuration with the given [key].
|
||||
* If the [key] does not exists, an [InvalidConfigurationKeyException] will be thrown.
|
||||
*/
|
||||
fun get(key: String): Configuration
|
||||
|
||||
/** Gets the configuration with the given [type]. */
|
||||
fun get(type: ConfigurationType): Configuration
|
||||
|
||||
/** Sets the content of each configuration in the given [configurations] list. */
|
||||
fun set(configurations: List<ConfigurationDto>)
|
||||
|
||||
/**
|
||||
* Sets the content of the configuration matching the given [configuration].
|
||||
* If the given key does not exists, an [InvalidConfigurationKeyException] will be thrown.
|
||||
*/
|
||||
fun set(configuration: ConfigurationDto)
|
||||
|
||||
/** Sets the content of the configuration with the given [type]. */
|
||||
fun set(type: ConfigurationType, content: String)
|
||||
|
||||
/** Sets the content of the configuration matching the given [configuration] with a given image. */
|
||||
fun set(configuration: ConfigurationImageDto)
|
||||
}
|
||||
|
||||
const val CONFIGURATION_LOGO_FILE_PATH = "images/logo"
|
||||
const val CONFIGURATION_ICON_FILE_PATH = "images/icon"
|
||||
const val CONFIGURATION_FORMATTED_LIST_DELIMITER = ';'
|
||||
|
||||
@Service
|
||||
class ConfigurationServiceImpl(
|
||||
@Lazy private val repository: ConfigurationRepository,
|
||||
private val fileService: FileService,
|
||||
private val fileConfiguration: FileConfiguration,
|
||||
private val creProperties: CreProperties,
|
||||
private val databaseProperties: DatabaseUpdaterProperties,
|
||||
private val buildInfo: BuildProperties
|
||||
) : ConfigurationService {
|
||||
override fun getAll() =
|
||||
ConfigurationType.values().mapNotNull {
|
||||
try {
|
||||
get(it)
|
||||
} catch (_: ConfigurationNotSetException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAll(formattedKeyList: String) =
|
||||
formattedKeyList.split(CONFIGURATION_FORMATTED_LIST_DELIMITER).map(this::get)
|
||||
|
||||
override fun get(key: String) =
|
||||
get(key.toConfigurationType())
|
||||
|
||||
override fun get(type: ConfigurationType) = when {
|
||||
type.computed -> getComputedConfiguration(type)
|
||||
type.file -> fileConfiguration.get(type)
|
||||
!emergencyMode -> repository.findByIdOrNull(type.key)?.toConfiguration()
|
||||
else -> null
|
||||
} ?: throw ConfigurationNotSetException(type)
|
||||
|
||||
override fun set(configurations: List<ConfigurationDto>) {
|
||||
configurations.forEach(this::set)
|
||||
}
|
||||
|
||||
override fun set(configuration: ConfigurationDto) = with(configuration) {
|
||||
set(key.toConfigurationType(), content)
|
||||
}
|
||||
|
||||
override fun set(type: ConfigurationType, content: String) {
|
||||
when {
|
||||
type.computed -> throw CannotSetComputedConfigurationException(type)
|
||||
type.file -> fileConfiguration.set(type, content)
|
||||
!emergencyMode -> repository.save(configuration(type, content).toEntity())
|
||||
}
|
||||
}
|
||||
|
||||
override fun set(configuration: ConfigurationImageDto) {
|
||||
val filePath = when (val configurationType = configuration.key.toConfigurationType()) {
|
||||
ConfigurationType.INSTANCE_LOGO_PATH -> CONFIGURATION_LOGO_FILE_PATH
|
||||
ConfigurationType.INSTANCE_ICON_PATH -> CONFIGURATION_ICON_FILE_PATH
|
||||
else -> throw InvalidImageConfigurationException(configurationType)
|
||||
}
|
||||
|
||||
fileService.write(configuration.image, filePath, true)
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
fun initializeProperties() {
|
||||
ConfigurationType.values().filter { !it.computed }.forEach {
|
||||
try {
|
||||
get(it)
|
||||
} catch (_: ConfigurationNotSetException) {
|
||||
set(it, it.defaultContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ConfigurationType.defaultContent: String
|
||||
get() = when (this) {
|
||||
ConfigurationType.INSTANCE_NAME -> "Color Recipes Explorer"
|
||||
ConfigurationType.INSTANCE_LOGO_PATH -> "images/logo"
|
||||
ConfigurationType.INSTANCE_ICON_PATH -> "images/icon"
|
||||
ConfigurationType.INSTANCE_URL -> creProperties.deploymentUrl
|
||||
ConfigurationType.DATABASE_URL -> databaseProperties.url
|
||||
ConfigurationType.DATABASE_USER -> databaseProperties.username
|
||||
ConfigurationType.DATABASE_PASSWORD -> databaseProperties.password
|
||||
ConfigurationType.RECIPE_APPROBATION_EXPIRATION -> period(months = 4)
|
||||
ConfigurationType.TOUCH_UP_KIT_CACHE_PDF -> "true"
|
||||
ConfigurationType.TOUCH_UP_KIT_EXPIRATION -> period(months = 1)
|
||||
else -> ""
|
||||
}
|
||||
|
||||
private fun getComputedConfiguration(key: ConfigurationType) = configuration(
|
||||
key, when (key) {
|
||||
ConfigurationType.EMERGENCY_MODE_ENABLED -> emergencyMode
|
||||
ConfigurationType.BUILD_VERSION -> buildInfo.version
|
||||
ConfigurationType.BUILD_TIME -> LocalDate.ofInstant(buildInfo.time, ZoneId.systemDefault()).toString()
|
||||
ConfigurationType.DATABASE_SUPPORTED_VERSION -> SUPPORTED_DATABASE_VERSION
|
||||
ConfigurationType.JAVA_VERSION -> Runtime.version()
|
||||
ConfigurationType.OPERATING_SYSTEM -> "${System.getProperty("os.name")} ${System.getProperty("os.version")} ${
|
||||
System.getProperty(
|
||||
"os.arch"
|
||||
)
|
||||
}"
|
||||
else -> throw IllegalArgumentException("Cannot get the value of the configuration with the key ${key.key} because it is not a computed configuration")
|
||||
}.toString()
|
||||
)
|
||||
|
||||
private fun period(days: Int = 0, months: Int = 0, years: Int = 0) =
|
||||
Period.of(days, months, years).toString()
|
||||
}
|
|
@ -3,6 +3,7 @@ package dev.fyloz.colorrecipesexplorer.service
|
|||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository
|
||||
import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH
|
||||
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
||||
import io.jsonwebtoken.lang.Assert
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
|
|
|
@ -4,6 +4,7 @@ import dev.fyloz.colorrecipesexplorer.model.*
|
|||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.or
|
||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.setAll
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.context.annotation.Profile
|
||||
|
|
|
@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
|||
import dev.fyloz.colorrecipesexplorer.model.touchupkit.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository
|
||||
import dev.fyloz.colorrecipesexplorer.rest.TOUCH_UP_KIT_CONTROLLER_PATH
|
||||
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.*
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
|
|
|
@ -36,3 +36,10 @@ fun <T> MutableCollection<T>.setAll(elements: Collection<T>) {
|
|||
this.clear()
|
||||
this.addAll(elements)
|
||||
}
|
||||
|
||||
/** Removes and returns all elements of a [MutableCollection] matching the given [predicate]. */
|
||||
inline fun <T> MutableCollection<T>.excludeAll(predicate: (T) -> Boolean): Iterable<T> {
|
||||
val matching = this.filter(predicate)
|
||||
this.removeAll(matching)
|
||||
return matching
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils
|
||||
|
||||
import java.time.Period
|
||||
|
||||
fun period(days: Int = 0, months: Int = 0, years: Int = 0): Period =
|
||||
Period.of(days, months, years)
|
||||
|
||||
val Int.months: Period
|
||||
get() = period(months = this)
|
|
@ -1,11 +1,11 @@
|
|||
# PORT
|
||||
server.port=9090
|
||||
# CRE
|
||||
cre.server.working-directory=data
|
||||
cre.server.deployment-url=http://localhost:9090
|
||||
cre.server.cache-generated-files=true
|
||||
cre.server.data-directory=data
|
||||
cre.server.config-directory=config
|
||||
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE
|
||||
cre.security.jwt-duration=18000000
|
||||
cre.security.aes-secret=blabla
|
||||
# Root user
|
||||
cre.security.root.id=9999
|
||||
cre.security.root.password=password
|
||||
|
|
Loading…
Reference in New Issue