Merge branch 'features' into 'master'

Ajout de l'API de configuration

See merge request color-recipes-explorer/backend!31
This commit is contained in:
William Nolin 2021-05-28 23:26:08 +00:00
commit e73fd5f44d
43 changed files with 1065 additions and 156 deletions

2
.gitignore vendored
View File

@ -14,3 +14,5 @@ dist/
out/
/src/main/resources/angular/static/*
config.properties

View File

@ -48,7 +48,7 @@ package:
ARTIFACT_NAME: "ColorRecipesExplorer-backend-$CI_PIPELINE_IID"
script:
- docker rm $PACKAGE_CONTAINER_NAME || true
- docker run --name $PACKAGE_CONTAINER_NAME $CI_REGISTRY_IMAGE_GRADLE gradle bootJar
- docker run --name $PACKAGE_CONTAINER_NAME $CI_REGISTRY_IMAGE_GRADLE gradle bootJar -Pversion=$CI_PIPELINE_IID
- docker cp $PACKAGE_CONTAINER_NAME:/usr/src/cre/build/libs/ColorRecipesExplorer.jar $ARTIFACT_NAME.jar
- docker build -t $CI_REGISTRY_IMAGE_BACKEND --build-arg JDK_VERSION=$JDK_VERSION --build-arg PORT=$PORT --build-arg ARTIFACT_NAME=$ARTIFACT_NAME .
- docker push $CI_REGISTRY_IMAGE_BACKEND
@ -81,4 +81,4 @@ deploy:
script:
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker stop $DEPLOYED_CONTAINER_NAME || true && docker rm $DEPLOYED_CONTAINER_NAME || true"
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY && docker pull $CI_REGISTRY_IMAGE_BACKEND"
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker run -d -p $PORT:$PORT --name=$DEPLOYED_CONTAINER_NAME -v $DATA_VOLUME:/usr/bin/cre/data -e spring_profiles_active=$SPRING_PROFILES -e spring_datasource_username=$DB_USERNAME -e spring_datasource_password=$DB_PASSWORD -e spring_datasource_url=$DB_URL -e cre_server_deployment_url=$DEPLOYMENT_URL -e databaseupdater_username=$DB_UPDATE_USERNAME -e databaseupdater_password=$DB_UPDATE_PASSWORD $CI_REGISTRY_IMAGE_BACKEND"
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker run -d -p $PORT:$PORT --name=$DEPLOYED_CONTAINER_NAME -v $DATA_VOLUME:/usr/bin/cre/data -e spring_profiles_active=$SPRING_PROFILES -e $CI_REGISTRY_IMAGE_BACKEND"

View File

@ -58,6 +58,12 @@ dependencies {
runtimeOnly("mysql:mysql-connector-java:8.0.22")
runtimeOnly("org.postgresql:postgresql:42.2.16")
runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre11")
implementation("org.springframework.cloud:spring-cloud-starter:2.2.8.RELEASE")
}
springBoot {
buildInfo()
}
java {

View File

@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.service.RecipeService;
import dev.fyloz.colorrecipesexplorer.xlsx.XlsxExporter;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
@ -14,6 +15,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Service
@Profile("!emergency")
public class XlsService {
private final RecipeService recipeService;

View File

@ -1,20 +1,37 @@
package dev.fyloz.colorrecipesexplorer
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
import dev.fyloz.colorrecipesexplorer.config.ApplicationInitializer
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.context.ConfigurableApplicationContext
@SpringBootApplication(exclude = [LiquibaseAutoConfiguration::class])
@EnableConfigurationProperties(
MaterialTypeProperties::class,
CreProperties::class,
DatabaseUpdaterProperties::class
)
class ColorRecipesExplorerApplication
var emergencyMode = false
private lateinit var context: ConfigurableApplicationContext
private lateinit var classLoader: ClassLoader
fun main() {
runApplication<ColorRecipesExplorerApplication>()
classLoader = Thread.currentThread().contextClassLoader
context = runApplication()
}
fun restartApplication(enableEmergencyMode: Boolean = false) {
val thread = Thread {
emergencyMode = enableEmergencyMode
context.close()
context = runApplication()
}
thread.contextClassLoader = classLoader
thread.isDaemon = false
thread.start()
}
private fun runApplication() =
SpringApplicationBuilder(ColorRecipesExplorerApplication::class.java).apply {
listeners(ApplicationInitializer())
}.run()

View File

@ -1,37 +1,82 @@
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 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.core.env.Environment
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
const val ENV_VAR_ENABLE_DATABASE_UPDATE_NAME = "CRE_ENABLE_DB_UPDATE"
val DATABASE_NAME_REGEX = Regex("(\\w+)$")
@Profile("!emergency")
@Configuration
@DependsOn("configurationsInitializer")
class DataSourceConfiguration {
@Bean(name = ["dataSource"])
@ConfigurationProperties(prefix = "spring.datasource")
fun customDataSource(
logger: Logger,
environment: Environment,
databaseUpdaterProperties: DatabaseUpdaterProperties
logger: Logger,
environment: ConfigurableEnvironment,
fileConfiguration: FileConfiguration,
databaseUpdaterProperties: DatabaseUpdaterProperties
): DataSource {
val databaseUrl: String = environment.getProperty("spring.datasource.url")!!
fun getConfiguration(type: ConfigurationType, defaultProperty: String) =
fileConfiguration.get(type)?.content ?: defaultProperty
runDatabaseVersionCheck(logger, databaseUrl, databaseUpdaterProperties)
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)
try {
runDatabaseVersionCheck(logger, databaseUrl, DatabaseUpdaterProperties().apply {
url = databaseUrl
username = databaseUsername
password = databasePassword
})
} catch (ex: Exception) {
logger.error("Could not access database, restarting in emergency mode...", ex)
emergencyMode = true
return emergencyDataSource()
}
return DataSourceBuilder
.create()
.url(databaseUrl) // Hikari won't start without that
.build()
.create()
.url(databaseUrl)
.username(databaseUsername)
.password(databasePassword)
.driverClassName(getDriverClassName(databaseUrl))
.build()
}
private fun emergencyDataSource() = with("jdbc:h2:mem:emergency") {
DataSourceBuilder
.create()
.url(this)
.driverClassName(getDriverClassName(this))
.username("sa")
.password("")
.build()
}
private fun getDriverClassName(url: String) = when {
url.startsWith("jdbc:postgres") -> "org.postgresql.Driver"
url.startsWith("jdbc:mssql") -> "com.microsoft.sqlserver.jdbc.SQLServerDriver"
url.startsWith("jdbc:mysql") -> "com.mysql.cj.jdbc.Driver"
url.startsWith("jdbc:h2") -> "org.h2.Driver"
else -> "org.h2.Driver"
}
}
@ -75,24 +120,24 @@ fun runDatabaseUpdate(logger: Logger, database: CreDatabase) {
}
fun getDatabase(
databaseUrl: String,
databaseUpdaterProperties: DatabaseUpdaterProperties,
logger: Logger
databaseUrl: String,
databaseUpdaterProperties: DatabaseUpdaterProperties,
logger: Logger
): CreDatabase {
val databaseName =
(DATABASE_NAME_REGEX.find(databaseUrl) ?: throw DatabaseVersioningException.InvalidUrl(databaseUrl)).value
(DATABASE_NAME_REGEX.find(databaseUrl) ?: throw DatabaseVersioningException.InvalidUrl(databaseUrl)).value
return CreDatabase(
databaseContext(
properties = databaseUpdaterProperties(
targetVersion = SUPPORTED_DATABASE_VERSION,
url = databaseUrl.removeSuffix(databaseName),
dbName = databaseName,
username = databaseUpdaterProperties.username,
password = databaseUpdaterProperties.password
),
logger
)
databaseContext(
properties = databaseUpdaterProperties(
targetVersion = SUPPORTED_DATABASE_VERSION,
url = databaseUrl.removeSuffix(databaseName),
dbName = databaseName,
username = databaseUpdaterProperties.username,
password = databaseUpdaterProperties.password
),
logger
)
)
}
@ -101,7 +146,7 @@ fun throwUnsupportedDatabaseVersion(version: Int, logger: Logger) {
logger.error("Version $version of the database is not supported; Only version $SUPPORTED_DATABASE_VERSION is currently supported; Update this application to use the database.")
} else {
logger.error(
"""Version $version of the database is not supported; Only version $SUPPORTED_DATABASE_VERSION is currently supported.
"""Version $version of the database is not supported; Only version $SUPPORTED_DATABASE_VERSION is currently supported.
|You can update the database to the supported version by either:
| - Setting the environment variable '$ENV_VAR_ENABLE_DATABASE_UPDATE_NAME' to '1' to update the database automatically
| - Updating the database manually with the database manager utility (https://git.fyloz.dev/color-recipes-explorer/database-manager)
@ -113,8 +158,9 @@ fun throwUnsupportedDatabaseVersion(version: Int, logger: Logger) {
throw DatabaseVersioningException.UnsupportedDatabaseVersion(version)
}
@ConfigurationProperties(prefix = "databaseupdater")
@ConfigurationProperties(prefix = "cre.database")
class DatabaseUpdaterProperties {
var url: String = ""
var username: String = ""
var password: String = ""
}
@ -122,5 +168,5 @@ class DatabaseUpdaterProperties {
sealed class DatabaseVersioningException(message: String) : Exception(message) {
class InvalidUrl(url: String) : DatabaseVersioningException("Invalid database url: $url")
class UnsupportedDatabaseVersion(version: Int) :
DatabaseVersioningException("Unsupported database version: $version; Only version $SUPPORTED_DATABASE_VERSION is currently supported")
DatabaseVersioningException("Unsupported database version: $version; Only version $SUPPORTED_DATABASE_VERSION is currently supported")
}

View File

@ -2,23 +2,50 @@ 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 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")
}
}
}

View File

@ -0,0 +1,54 @@
package dev.fyloz.colorrecipesexplorer.config
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
import dev.fyloz.colorrecipesexplorer.model.configuration
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.*
@Configuration
class ConfigurationsInitializer {
@Bean
fun fileConfiguration() = FileConfiguration()
}
const val FILE_CONFIGURATION_PATH = "config.properties"
const val FILE_CONFIGURATION_COMMENT = "---Color Recipes Explorer configuration---"
class FileConfiguration {
val properties = Properties().apply {
with(File(FILE_CONFIGURATION_PATH)) {
if (!this.exists()) this.createNewFile()
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(FILE_CONFIGURATION_PATH).use {
properties.store(it, FILE_CONFIGURATION_COMMENT)
}
}
private fun configurationLastUpdateKey(key: String) = "$key.last-updated"
}

View File

@ -0,0 +1,95 @@
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.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
) : 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) {
http
.headers().frameOptions().disable()
.and()
.csrf().disable()
.cors()
.and()
.addFilter(
JwtAuthenticationFilter(
authenticationManager(),
securityConfigurationProperties
) { }
)
.addFilter(
JwtAuthorizationFilter(
securityConfigurationProperties,
authenticationManager(),
this::loadUserById
)
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("**").permitAll()
}
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())
}
}

View File

@ -1,12 +1,17 @@
package dev.fyloz.colorrecipesexplorer.config
import dev.fyloz.colorrecipesexplorer.ColorRecipesExplorerApplication
import dev.fyloz.colorrecipesexplorer.DatabaseUpdaterProperties
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
@EnableConfigurationProperties(MaterialTypeProperties::class, CreProperties::class, DatabaseUpdaterProperties::class)
class SpringConfiguration {
@Bean
fun logger(): Logger = LoggerFactory.getLogger(ColorRecipesExplorerApplication::class.java)

View File

@ -1,14 +1,13 @@
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.UserLoginRequest
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.service.UserService
import dev.fyloz.colorrecipesexplorer.service.UserServiceImpl
import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest
import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsService
import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsServiceImpl
import dev.fyloz.colorrecipesexplorer.service.UserService
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
@ -18,6 +17,7 @@ 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
@ -31,7 +31,7 @@ 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.User as SpringUser
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
@ -46,15 +46,17 @@ 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: CreUserDetailsServiceImpl,
@Lazy val userService: UserServiceImpl,
@Lazy val userDetailsService: CreUserDetailsService,
@Lazy val userService: UserService,
val environment: Environment,
val logger: Logger
) : WebSecurityConfigurerAdapter() {
@ -66,51 +68,56 @@ class WebSecurityConfig(
@Bean
fun passwordEncoder() =
BCryptPasswordEncoder()
BCryptPasswordEncoder()
@Bean
override fun authenticationManagerBean(): AuthenticationManager =
super.authenticationManagerBean()
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())
}
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>
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()
)
User(
id = credentials.id!!,
firstName = firstName,
lastName = lastName,
password = passwordEncoder().encode(credentials.password!!),
isSystemUser = true,
permissions = permissions.toMutableSet()
)
)
}
}
@ -122,37 +129,35 @@ class WebSecurityConfig(
override fun configure(http: HttpSecurity) {
http
.headers().frameOptions().disable()
.and()
.csrf().disable()
.addFilter(
JwtAuthenticationFilter(
authenticationManager(),
userService,
securityConfigurationProperties
.headers().frameOptions().disable()
.and()
.csrf().disable()
.addFilter(
JwtAuthenticationFilter(
authenticationManager(),
securityConfigurationProperties
) { userService.updateLastLoginTime(it) }
)
)
.addFilter(
JwtAuthorizationFilter(
userDetailsService,
securityConfigurationProperties,
authenticationManager()
.addFilter(
JwtAuthorizationFilter(
securityConfigurationProperties,
authenticationManager()
) { userDetailsService.loadUserById(it, false) }
)
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
if (!debugMode) {
http.authorizeRequests()
.antMatchers("/api/login").permitAll()
.antMatchers("/api/logout").authenticated()
.antMatchers("/api/user/current").authenticated()
.anyRequest().authenticated()
.antMatchers("/api/login").permitAll()
.antMatchers("/api/logout").authenticated()
.antMatchers("/api/user/current").authenticated()
.anyRequest().authenticated()
} else {
http
.cors()
.and()
.authorizeRequests()
.antMatchers("**").permitAll()
.cors()
.and()
.authorizeRequests()
.antMatchers("**").permitAll()
}
}
}
@ -160,9 +165,9 @@ class WebSecurityConfig(
@Component
class RestAuthenticationEntryPoint : AuthenticationEntryPoint {
override fun commence(
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException
) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
}
@ -172,8 +177,8 @@ val blacklistedJwtTokens = mutableListOf<String>()
class JwtAuthenticationFilter(
private val authManager: AuthenticationManager,
private val userService: UserService,
private val securityConfigurationProperties: SecurityConfigurationProperties
private val securityConfigurationProperties: SecurityConfigurationProperties,
private val updateUserLoginTime: (Long) -> Unit
) : UsernamePasswordAuthenticationFilter() {
private var debugMode = false
@ -188,31 +193,31 @@ class JwtAuthenticationFilter(
}
override fun successfulAuthentication(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain,
authResult: Authentication
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
userService.updateLastLoginTime(userId.toLong())
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()
.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"
"$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; SameSite=strict"
if (!debugMode) bearerCookie += "; Secure;"
response.addHeader(
"Set-Cookie",
bearerCookie
"Set-Cookie",
bearerCookie
)
response.addHeader(authorizationCookieName, "Bearer $token")
response.addHeader("X-Authentication-Expiration", "$expirationMs")
@ -220,9 +225,9 @@ class JwtAuthenticationFilter(
}
class JwtAuthorizationFilter(
private val userDetailsService: CreUserDetailsService,
private val securityConfigurationProperties: SecurityConfigurationProperties,
authenticationManager: AuthenticationManager
authenticationManager: AuthenticationManager,
private val loadUserById: (Long) -> UserDetails
) : BasicAuthenticationFilter(authenticationManager) {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
fun tryLoginFromBearer(): Boolean {
@ -260,10 +265,10 @@ class JwtAuthorizationFilter(
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
return try {
val userId = Jwts.parser()
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
if (userId != null) getAuthenticationToken(userId) else null
} catch (_: ExpiredJwtException) {
null
@ -271,7 +276,7 @@ class JwtAuthorizationFilter(
}
private fun getAuthenticationToken(userId: String): UsernamePasswordAuthenticationToken? = try {
val userDetails = userDetailsService.loadUserById(userId.toLong(), false)
val userDetails = loadUserById(userId.toLong())
UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities)
} catch (_: NotFoundException) {
null

View File

@ -1,14 +1,33 @@
package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.RestException
import org.springframework.http.HttpStatus
import org.springframework.web.multipart.MultipartFile
import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Table
import javax.validation.constraints.NotBlank
data class Configuration(
@JsonIgnore
val type: ConfigurationType,
val content: String,
val lastUpdated: LocalDateTime
) {
val key = type.key
val requireRestart = type.requireRestart
val editable = !type.computed
fun toEntity() =
ConfigurationEntity(key, content, lastUpdated)
}
@Entity
@Table(name = "configuration")
data class Configuration(
data class ConfigurationEntity(
@Id
@Column(name = "config_key")
val key: String,
@ -17,4 +36,111 @@ data class Configuration(
@Column(name = "last_updated")
val lastUpdated: LocalDateTime
) {
fun toConfiguration() =
configuration(key.toConfigurationType(), content, lastUpdated)
override fun equals(other: Any?) =
other is ConfigurationEntity && key == other.key && content == other.content
override fun hashCode(): Int {
var result = key.hashCode()
result = 31 * result + content.hashCode()
return result
}
}
data class ConfigurationDto(
val key: String,
@NotBlank
val content: String
)
data class ConfigurationImageDto(
val key: String,
val image: MultipartFile
)
fun configuration(
type: ConfigurationType,
content: String,
lastUpdated: LocalDateTime? = null
) = Configuration(type, content, lastUpdated ?: LocalDateTime.now())
enum class ConfigurationType(
val key: String,
val computed: Boolean = false,
val file: Boolean = false,
val requireRestart: Boolean = false,
val public: 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),
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_SUPPORTED_VERSION("database.version.supported", computed = true),
TOUCH_UP_KIT_CACHE_PDF("touchupkit.pdf.cache"),
EMERGENCY_MODE_ENABLED("env.emergency", computed = true, public = true),
VERSION("env.version", computed = true),
JAVA_VERSION("env.java.version", computed = true),
OPERATING_SYSTEM("env.os", computed = true)
;
override fun toString() = key
}
fun String.toConfigurationType() =
ConfigurationType.values().firstOrNull { it.key == this }
?: throw InvalidConfigurationKeyException(this)
class InvalidConfigurationKeyException(val key: String) :
RestException(
"invalid-configuration-key",
"Invalid configuration key",
HttpStatus.BAD_REQUEST,
"The configuration key '$key' does not exists",
mapOf(
"key" to key
)
)
class InvalidImageConfigurationException(val type: ConfigurationType) :
RestException(
"invalid-configuration-image",
"Invalid image configuration",
HttpStatus.BAD_REQUEST,
"The configuration with the key '${type.key}' does not accept images as content",
mapOf(
"key" to type.key
)
)
class ConfigurationNotSetException(val type: ConfigurationType) :
RestException(
"unset-configuration",
"Unset configuration",
HttpStatus.NOT_FOUND,
"The configuration with the key '${type.key}' is not set",
mapOf(
"key" to type.key
)
)
class CannotSetComputedConfigurationException(val type: ConfigurationType) :
RestException(
"cannot-set-computed-configuration",
"Cannot set computed configuration",
HttpStatus.BAD_REQUEST,
"The configuration with the key '${type.key}' is a computed configuration and cannot be modified",
mapOf(
"key" to type.key
)
)

View File

@ -6,7 +6,7 @@ import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.account.group
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.time.LocalDate
@ -67,8 +67,8 @@ data class Recipe(
fun groupInformationForGroup(groupId: Long) =
groupsInformation.firstOrNull { it.group.id == groupId }
fun imageUrl(name: String) =
"${CRE_PROPERTIES.deploymentUrl}$FILE_CONTROLLER_PATH?path=${
fun imageUrl(deploymentUrl: String, name: String) =
"$deploymentUrl$FILE_CONTROLLER_PATH?path=${
URLEncoder.encode(
"${this.imagesDirectoryPath}/$name",
StandardCharsets.UTF_8

View File

@ -0,0 +1,7 @@
package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.ConfigurationEntity
import org.springframework.data.jpa.repository.JpaRepository
interface ConfigurationRepository : JpaRepository<ConfigurationEntity, String> {
}

View File

@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.service.UserService
import dev.fyloz.colorrecipesexplorer.service.GroupService
import org.springframework.context.annotation.Profile
import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
@ -18,6 +19,7 @@ private const val GROUP_CONTROLLER_PATH = "api/user/group"
@RestController
@RequestMapping(USER_CONTROLLER_PATH)
@Profile("!emergency")
class UserController(private val userService: UserService) {
@GetMapping
@PreAuthorizeViewUsers
@ -93,6 +95,7 @@ class UserController(private val userService: UserService) {
@RestController
@RequestMapping(GROUP_CONTROLLER_PATH)
@Profile("!emergency")
class GroupsController(
private val groupService: GroupService,
private val userService: UserService
@ -155,10 +158,11 @@ class GroupsController(
@RestController
@RequestMapping("api")
@Profile("!emergency")
class LogoutController(private val userService: UserService) {
@GetMapping("logout")
fun logout(request: HttpServletRequest) =
ok<Void> {
ok {
userService.logout(request)
}
}

View File

@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.model.Company
import dev.fyloz.colorrecipesexplorer.model.CompanySaveDto
import dev.fyloz.colorrecipesexplorer.model.CompanyUpdateDto
import dev.fyloz.colorrecipesexplorer.service.CompanyService
import org.springframework.context.annotation.Profile
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
import javax.validation.Valid
@ -13,6 +14,7 @@ private const val COMPANY_CONTROLLER_PATH = "api/company"
@RestController
@RequestMapping(COMPANY_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorizeViewCatalog
class CompanyController(private val companyService: CompanyService) {
@GetMapping

View File

@ -0,0 +1,56 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.model.Configuration
import dev.fyloz.colorrecipesexplorer.model.ConfigurationDto
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 org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.Authentication
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import javax.validation.constraints.NotBlank
@RestController
@RequestMapping("api/config")
class ConfigurationController(val configurationService: ConfigurationService) {
@GetMapping
fun getAll(@RequestParam(required = false) keys: String?, authentication: Authentication?) =
ok(with(configurationService) {
if (keys != null) getAll(keys) else getAll()
}.filter {
authentication.hasAuthority(it)
})
@GetMapping("{key}")
fun get(@PathVariable key: String, authentication: Authentication?) = with(configurationService.get(key)) {
if (authentication.hasAuthority(this)) ok(this) else forbidden()
}
@PutMapping
@PreAuthorize("hasAuthority('ADMIN')")
fun set(@RequestBody configurations: List<ConfigurationDto>) = noContent {
configurationService.set(configurations)
}
@PutMapping("image")
@PreAuthorize("hasAuthority('ADMIN')")
fun setImage(@RequestParam @NotBlank key: String, @RequestParam @NotBlank image: MultipartFile) = noContent {
configurationService.set(ConfigurationImageDto(key, image))
}
@PostMapping("restart")
@PreAuthorize("hasAuthority('ADMIN')")
fun restart() = noContent {
restartApplication()
}
}
private fun Authentication?.hasAuthority(configuration: Configuration) = when {
configuration.type.public -> true
this != null && Permission.ADMIN.toAuthority() in this.authorities -> true
else -> false
}

View File

@ -1,7 +1,7 @@
package dev.fyloz.colorrecipesexplorer.rest.files
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.rest.noContent
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
import dev.fyloz.colorrecipesexplorer.service.ConfigurationService
import dev.fyloz.colorrecipesexplorer.service.files.FileService
import org.springframework.core.io.ByteArrayResource
import org.springframework.http.MediaType
@ -18,10 +18,9 @@ private const val DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE
@RequestMapping(FILE_CONTROLLER_PATH)
class FileController(
private val fileService: FileService,
private val creProperties: CreProperties
private val configService: ConfigurationService
) {
@GetMapping(produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE])
@PreAuthorize("hasAnyAuthority('READ_FILE')")
fun upload(
@RequestParam path: String,
@RequestParam(required = false) mediaType: String?
@ -55,7 +54,7 @@ class FileController(
private fun created(path: String): ResponseEntity<Void> =
ResponseEntity
.created(URI.create("${creProperties.deploymentUrl}$FILE_CONTROLLER_PATH?path=$path"))
.created(URI.create("${configService.get(ConfigurationType.INSTANCE_URL)}$FILE_CONTROLLER_PATH?path=$path"))
.build()
private fun getFileNameFromPath(path: String) =

View File

@ -3,6 +3,7 @@ package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.model.MixDeductDto
import dev.fyloz.colorrecipesexplorer.service.InventoryService
import org.springframework.context.annotation.Profile
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.PutMapping
@ -14,6 +15,7 @@ private const val INVENTORY_CONTROLLER_PATH = "api/inventory"
@RestController
@RequestMapping(INVENTORY_CONTROLLER_PATH)
@Profile("!emergency")
class InventoryController(
private val inventoryService: InventoryService
) {

View File

@ -4,6 +4,7 @@ import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.service.MaterialService
import org.springframework.context.annotation.Profile
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
@ -16,6 +17,7 @@ private const val MATERIAL_CONTROLLER_PATH = "api/material"
@RestController
@RequestMapping(MATERIAL_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorizeViewCatalog
class MaterialController(
private val materialService: MaterialService

View File

@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.model.MaterialType
import dev.fyloz.colorrecipesexplorer.model.MaterialTypeSaveDto
import dev.fyloz.colorrecipesexplorer.model.MaterialTypeUpdateDto
import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService
import org.springframework.context.annotation.Profile
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
import javax.validation.Valid
@ -13,6 +14,7 @@ private const val MATERIAL_TYPE_CONTROLLER_PATH = "api/materialtype"
@RestController
@RequestMapping(MATERIAL_TYPE_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorizeViewCatalog
class MaterialTypeController(private val materialTypeService: MaterialTypeService) {
@GetMapping

View File

@ -6,6 +6,7 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.service.MixService
import dev.fyloz.colorrecipesexplorer.service.RecipeImageService
import dev.fyloz.colorrecipesexplorer.service.RecipeService
import org.springframework.context.annotation.Profile
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
@ -19,6 +20,7 @@ private const val MIX_CONTROLLER_PATH = "api/recipe/mix"
@RestController
@RequestMapping(RECIPE_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorizeViewRecipes
class RecipeController(
private val recipeService: RecipeService,
@ -84,6 +86,7 @@ class RecipeController(
@RestController
@RequestMapping(MIX_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorizeViewRecipes
class MixController(private val mixService: MixService) {
@GetMapping("{id}")

View File

@ -19,7 +19,7 @@ fun <T> ok(body: T, headers: HttpHeaders): ResponseEntity<T> =
ResponseEntity(body, headers, HttpStatus.OK)
/** Executes the given [action] then returns an HTTP OK [ResponseEntity] form the given [body]. */
fun <T> ok(action: () -> Unit): ResponseEntity<T> {
fun ok(action: () -> Unit): ResponseEntity<Void> {
action()
return ResponseEntity.ok().build()
}

View File

@ -4,6 +4,7 @@ import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitOutputDto
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitSaveDto
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitUpdateDto
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TouchUpKitService
import org.springframework.context.annotation.Profile
import org.springframework.core.io.ByteArrayResource
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
@ -15,6 +16,7 @@ const val TOUCH_UP_KIT_CONTROLLER_PATH = "/api/touchupkit"
@RestController
@RequestMapping(TOUCH_UP_KIT_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorize("hasAuthority('VIEW_TOUCH_UP_KITS')")
class TouchUpKitController(
private val touchUpKitService: TouchUpKitService

View File

@ -8,6 +8,7 @@ import dev.fyloz.colorrecipesexplorer.model.validation.or
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
@ -74,6 +75,7 @@ interface CreUserDetailsService : UserDetailsService {
}
@Service
@Profile("!emergency")
class UserServiceImpl(
userRepository: UserRepository,
@Lazy val groupService: GroupService,
@ -229,6 +231,7 @@ class UserServiceImpl(
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
@Service
@Profile("!emergency")
class GroupServiceImpl(
private val userService: UserService,
groupRepository: GroupRepository
@ -298,6 +301,7 @@ class GroupServiceImpl(
}
@Service
@Profile("!emergency")
class CreUserDetailsServiceImpl(
private val userService: UserService
) :

View File

@ -3,6 +3,7 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
interface CompanyService :
@ -12,6 +13,7 @@ interface CompanyService :
}
@Service
@Profile("!emergency")
class CompanyServiceImpl(
companyRepository: CompanyRepository,
@Lazy val recipeService: RecipeService

View File

@ -0,0 +1,152 @@
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 dev.fyloz.colorrecipesexplorer.service.files.FileService
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.io.File
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.TOUCH_UP_KIT_CACHE_PDF -> "true"
else -> ""
}
private fun getComputedConfiguration(key: ConfigurationType) = configuration(
key, when (key) {
ConfigurationType.EMERGENCY_MODE_ENABLED -> emergencyMode
ConfigurationType.VERSION -> buildInfo.version
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()
)
}

View File

@ -3,6 +3,7 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import javax.transaction.Transactional
@ -25,6 +26,7 @@ interface InventoryService {
}
@Service
@Profile("!emergency")
class InventoryServiceImpl(
private val materialService: MaterialService,
private val mixService: MixService

View File

@ -2,11 +2,11 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH
import dev.fyloz.colorrecipesexplorer.service.files.FileService
import io.jsonwebtoken.lang.Assert
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
@ -33,12 +33,14 @@ interface MaterialService :
}
@Service
@Profile("!emergency")
class MaterialServiceImpl(
materialRepository: MaterialRepository,
val recipeService: RecipeService,
val mixService: MixService,
@Lazy val materialTypeService: MaterialTypeService,
val fileService: FileService
val fileService: FileService,
val configService: ConfigurationService
) :
AbstractExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialOutputDto, MaterialRepository>(
materialRepository
@ -57,7 +59,7 @@ class MaterialServiceImpl(
isMixType = this.isMixType,
materialType = this.materialType!!,
simdutUrl = if (fileService.exists(this.simdutFilePath))
"${CRE_PROPERTIES.deploymentUrl}$FILE_CONTROLLER_PATH?path=${
"${configService.get(ConfigurationType.INSTANCE_URL)}$FILE_CONTROLLER_PATH?path=${
URLEncoder.encode(
this.simdutFilePath,
StandardCharsets.UTF_8

View File

@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.validation.isNotNullAndNotBlank
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
interface MaterialTypeService :
@ -26,6 +27,7 @@ interface MaterialTypeService :
}
@Service
@Profile("!emergency")
class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val materialService: MaterialService) :
AbstractExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository>(
repository

View File

@ -6,6 +6,7 @@ import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository
import dev.fyloz.colorrecipesexplorer.utils.findDuplicated
import dev.fyloz.colorrecipesexplorer.utils.hasGaps
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
@ -33,6 +34,7 @@ interface MixMaterialService : ModelService<MixMaterial, MixMaterialRepository>
}
@Service
@Profile("!emergency")
class MixMaterialServiceImpl(
mixMaterialRepository: MixMaterialRepository,
@Lazy val materialService: MaterialService

View File

@ -4,6 +4,7 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixRepository
import dev.fyloz.colorrecipesexplorer.utils.setAll
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
import javax.transaction.Transactional
@ -22,6 +23,7 @@ interface MixService : ExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixOu
}
@Service
@Profile("!emergency")
class MixServiceImpl(
mixRepository: MixRepository,
@Lazy val recipeService: RecipeService,

View File

@ -3,6 +3,7 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
interface MixTypeService : NamedModelService<MixType, MixTypeRepository> {
@ -26,6 +27,7 @@ interface MixTypeService : NamedModelService<MixType, MixTypeRepository> {
}
@Service
@Profile("!emergency")
class MixTypeServiceImpl(
mixTypeRepository: MixTypeRepository,
@Lazy val materialService: MaterialService,

View File

@ -7,6 +7,7 @@ import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.colorrecipesexplorer.service.files.FileService
import dev.fyloz.colorrecipesexplorer.utils.setAll
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
import java.io.File
@ -37,13 +38,15 @@ interface RecipeService :
}
@Service
@Profile("!emergency")
class RecipeServiceImpl(
recipeRepository: RecipeRepository,
val companyService: CompanyService,
val mixService: MixService,
val recipeStepService: RecipeStepService,
@Lazy val groupService: GroupService,
val recipeImageService: RecipeImageService
val recipeImageService: RecipeImageService,
val configService: ConfigurationService
) :
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeOutputDto, RecipeRepository>(
recipeRepository
@ -69,7 +72,7 @@ class RecipeServiceImpl(
}.toSet(),
this.groupsInformation,
recipeImageService.getAllImages(this)
.map { this.imageUrl(it) }
.map { this.imageUrl(configService.get(ConfigurationType.INSTANCE_URL).content, it) }
.toSet()
)
@ -206,6 +209,7 @@ const val RECIPE_IMAGE_ID_DELIMITER = "_"
const val RECIPE_IMAGE_EXTENSION = ".jpg"
@Service
@Profile("!emergency")
class RecipeImageServiceImpl(
val fileService: FileService
) : RecipeImageService {

View File

@ -6,6 +6,7 @@ import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository
import dev.fyloz.colorrecipesexplorer.utils.findDuplicated
import dev.fyloz.colorrecipesexplorer.utils.hasGaps
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
@ -22,6 +23,7 @@ interface RecipeStepService : ModelService<RecipeStep, RecipeStepRepository> {
}
@Service
@Profile("!emergency")
class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) :
AbstractModelService<RecipeStep, RecipeStepRepository>(recipeStepRepository),
RecipeStepService {

View File

@ -1,13 +1,16 @@
package dev.fyloz.colorrecipesexplorer.service.touchupkit
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
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.AbstractExternalModelService
import dev.fyloz.colorrecipesexplorer.service.ConfigurationService
import dev.fyloz.colorrecipesexplorer.service.ExternalModelService
import dev.fyloz.colorrecipesexplorer.service.files.FileService
import dev.fyloz.colorrecipesexplorer.utils.*
import org.springframework.context.annotation.Profile
import org.springframework.core.io.ByteArrayResource
import org.springframework.stereotype.Service
@ -34,10 +37,11 @@ interface TouchUpKitService :
}
@Service
@Profile("!emergency")
class TouchUpKitServiceImpl(
private val fileService: FileService,
touchUpKitRepository: TouchUpKitRepository,
private val creProperties: CreProperties,
private val configService: ConfigurationService,
touchUpKitRepository: TouchUpKitRepository
) : AbstractExternalModelService<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository>(
touchUpKitRepository
), TouchUpKitService {
@ -98,7 +102,7 @@ class TouchUpKitServiceImpl(
}
override fun generateJobPdfResource(job: String): ByteArrayResource {
if (creProperties.cacheGeneratedFiles) {
if (cacheGeneratedFiles()) {
with(job.pdfDocumentPath()) {
if (fileService.exists(this)) {
return fileService.read(this)
@ -112,7 +116,7 @@ class TouchUpKitServiceImpl(
}
override fun String.cachePdfDocument(document: PdfDocument) {
if (!creProperties.cacheGeneratedFiles) return
if (!cacheGeneratedFiles()) return
fileService.write(document.toByteArrayResource(), this.pdfDocumentPath(), true)
}
@ -121,5 +125,8 @@ class TouchUpKitServiceImpl(
"$TOUCH_UP_KIT_FILES_PATH/$this.pdf"
private fun TouchUpKit.pdfUrl() =
"${creProperties.deploymentUrl}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project"
"${configService.get(ConfigurationType.INSTANCE_URL)}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project"
private fun cacheGeneratedFiles() =
configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF).content == "true"
}

View File

@ -0,0 +1 @@
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration

View File

@ -1,6 +1,7 @@
spring.datasource.url=jdbc:mysql://172.66.1.1/cre
spring.datasource.username=root
spring.datasource.password=pass
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto=none
# Database manager
cre.database.url=mysql://localhost/cre
cre.database.username=root
cre.database.password=pass

View File

@ -17,9 +17,6 @@ entities.material-types.systemTypes[1].name=Base
entities.material-types.systemTypes[1].prefix=BAS
entities.material-types.systemTypes[1].usepercentages=false
entities.material-types.baseName=Base
# Database manager
databaseupdater.username=root
databaseupdater.password=pass
# DEBUG
spring.jpa.show-sql=false
# Do not modify
@ -33,3 +30,5 @@ spring.h2.console.enabled=false
spring.jackson.deserialization.fail-on-null-for-primitives=true
spring.jackson.default-property-inclusion=non_null
spring.profiles.active=@spring.profiles.active@
spring.datasource.continue-on-error=true

View File

@ -0,0 +1,255 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.FileConfiguration
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.ConfigurationRepository
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.time.LocalDateTime
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ConfigurationServiceTest {
private val repository = mockk<ConfigurationRepository>()
private val fileConfiguration = mockk<FileConfiguration>()
private val service = spyk(ConfigurationServiceImpl(repository, mockk(), fileConfiguration, mockk(), mockk(), mockk()))
@AfterEach
fun afterEach() {
clearAllMocks()
}
// getAll()
@Test
fun `getAll() gets the Configuration of each ConfigurationType`() {
every { service.get(any<ConfigurationType>()) } answers { throw ConfigurationNotSetException(this.args[0] as ConfigurationType) }
service.getAll()
verify {
service.getAll()
ConfigurationType.values().forEach {
service.get(it)
}
}
confirmVerified(service)
}
@Test
fun `getAll() only returns set configurations`() {
val unsetConfigurationTypes = listOf(
ConfigurationType.INSTANCE_NAME,
ConfigurationType.INSTANCE_LOGO_PATH,
ConfigurationType.INSTANCE_ICON_PATH
)
every { service.get(match<ConfigurationType> { it in unsetConfigurationTypes }) } answers {
throw ConfigurationNotSetException(this.firstArg() as ConfigurationType)
}
every { service.get(match<ConfigurationType> { it !in unsetConfigurationTypes }) } answers {
val type = firstArg<ConfigurationType>()
configuration(type, type.key)
}
val found = service.getAll()
assertFalse {
found.any {
it.type in unsetConfigurationTypes
}
}
verify {
service.getAll()
ConfigurationType.values().forEach {
service.get(it)
}
}
confirmVerified(service)
}
@Test
fun `getAll() only includes configurations matching the formatted formatted key list`() {
val configurationTypes = listOf(
ConfigurationType.INSTANCE_NAME,
ConfigurationType.INSTANCE_LOGO_PATH,
ConfigurationType.INSTANCE_ICON_PATH
)
val formattedKeyList = configurationTypes
.map { it.key }
.reduce { acc, s -> "$acc$CONFIGURATION_FORMATTED_LIST_DELIMITER$s" }
every { service.get(any<String>()) } answers {
val key = firstArg<String>()
configuration(key.toConfigurationType(), key)
}
val found = service.getAll(formattedKeyList)
assertTrue {
found.all { it.type in configurationTypes }
}
verify {
service.getAll(formattedKeyList)
configurationTypes.forEach {
service.get(it.key)
}
}
confirmVerified(service)
}
// get()
@Test
fun `get(key) calls get() with the ConfigurationType matching the given key`() {
val type = ConfigurationType.INSTANCE_ICON_PATH
val key = type.key
every { service.get(type) } answers {
val type = firstArg<ConfigurationType>()
configuration(type, type.key)
}
service.get(key)
verify {
service.get(key)
service.get(type)
}
confirmVerified(service)
}
@Test
fun `get(type) gets in the repository when the given ConfigurationType is not computed or a file property`() {
val type = ConfigurationType.INSTANCE_ICON_PATH
every { repository.findById(type.key) } returns Optional.of(
ConfigurationEntity(type.key, type.key, LocalDateTime.now())
)
val configuration = service.get(type)
assertTrue {
configuration.key == type.key
}
verify {
service.get(type)
repository.findById(type.key)
}
confirmVerified(service, repository)
}
@Test
fun `get(type) gets in the FileConfiguration when the gien ConfigurationType is a file property`() {
val type = ConfigurationType.DATABASE_URL
every { fileConfiguration.get(type) } returns configuration(type, type.key)
val configuration = service.get(type)
assertTrue {
configuration.key == type.key
}
verify {
service.get(type)
fileConfiguration.get(type)
}
verify(exactly = 0) {
repository.findById(type.key)
}
confirmVerified(service, fileConfiguration, repository)
}
@Test
fun `get(type) computes computed properties`() {
val type = ConfigurationType.JAVA_VERSION
val configuration = service.get(type)
assertTrue {
configuration.key == type.key
}
verify {
service.get(type)
}
verify(exactly = 0) {
repository.findById(type.key)
fileConfiguration.get(type)
}
confirmVerified(service, repository, fileConfiguration)
}
@Test
fun `get(type) throws ConfigurationNotSetException when the given ConfigurationType has no set configuration`() {
val type = ConfigurationType.INSTANCE_ICON_PATH
every { repository.findById(type.key) } returns Optional.empty()
with(assertThrows<ConfigurationNotSetException> { service.get(type) }) {
assertEquals(type, this.type)
}
verify {
service.get(type)
repository.findById(type.key)
}
}
@Test
fun `set() set the configuration in the FileConfiguration when the given ConfigurationType is a file configuration`() {
val type = ConfigurationType.DATABASE_URL
val content = "url"
every { fileConfiguration.set(type, content) } just runs
service.set(type, content)
verify {
service.set(type, content)
fileConfiguration.set(type, content)
}
confirmVerified(service, fileConfiguration)
}
@Test
fun `set() set the configuration in the repository when the given ConfigurationType is not a computed configuration of a file configuration`() {
val type = ConfigurationType.INSTANCE_ICON_PATH
val content = "path"
val configuration = configuration(type, content)
val entity = configuration.toEntity()
every { repository.save(entity) } returns entity
service.set(type, content)
verify {
service.set(type, content)
repository.save(entity)
}
confirmVerified(service, repository)
}
@Test
fun `set() throws CannotSetComputedConfigurationException when the given ConfigurationType is a computed configuration`() {
val type = ConfigurationType.JAVA_VERSION
val content = "5"
with(assertThrows<CannotSetComputedConfigurationException> { service.set(type, content) }) {
assertEquals(type, this.type)
}
verify {
service.set(type, content)
}
confirmVerified(service)
}
}

View File

@ -23,7 +23,7 @@ class MaterialServiceTest :
private val materialTypeService: MaterialTypeService = mock()
private val fileService: FileService = mock()
override val service: MaterialService =
spy(MaterialServiceImpl(repository, recipeService, mixService, materialTypeService, fileService))
spy(MaterialServiceImpl(repository, recipeService, mixService, materialTypeService, fileService, mock()))
override val entity: Material = material(id = 0L, name = "material")
private val entityOutput = materialOutputDto(entity)

View File

@ -27,7 +27,7 @@ class RecipeServiceTest :
private val groupService: GroupService = mock()
private val recipeStepService: RecipeStepService = mock()
override val service: RecipeService =
spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService, mock()))
spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService, mock(), mock()))
private val company: Company = company(id = 0L)
override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company)

View File

@ -1,7 +1,10 @@
package dev.fyloz.colorrecipesexplorer.service.files
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
import dev.fyloz.colorrecipesexplorer.model.configuration
import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository
import dev.fyloz.colorrecipesexplorer.service.ConfigurationService
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TOUCH_UP_TEXT_EN
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TOUCH_UP_TEXT_FR
import dev.fyloz.colorrecipesexplorer.service.touchupkit.TouchUpKitServiceImpl
@ -21,7 +24,8 @@ private class TouchUpKitServiceTestContext {
val creProperties = mockk<CreProperties> {
every { cacheGeneratedFiles } returns false
}
val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, touchUpKitRepository, creProperties))
val configService = mockk<ConfigurationService>(relaxed = true)
val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, configService, touchUpKitRepository))
val pdfDocumentData = mockk<ByteArrayResource>()
val pdfDocument = mockk<PdfDocument> {
mockkStatic(PdfDocument::toByteArrayResource)
@ -81,6 +85,7 @@ class TouchUpKitServiceTest {
every { creProperties.cacheGeneratedFiles } returns true
every { fileService.exists(any()) } returns true
every { fileService.read(any()) } returns pdfDocumentData
every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF, "true")
val redResource = touchUpKitService.generateJobPdfResource(job)
@ -109,6 +114,7 @@ class TouchUpKitServiceTest {
fun `cachePdfDocument() writes the given document to the FileService when cache is enabled`() {
test {
every { creProperties.cacheGeneratedFiles } returns true
every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF, "true")
with(touchUpKitService) {
job.cachePdfDocument(pdfDocument)