Merge remote-tracking branch 'origin/master'

# Conflicts:
#	.gitlab-ci.yml
This commit is contained in:
FyloZ 2021-05-28 19:59:16 -04:00
commit 6fbae96bde
71 changed files with 2377 additions and 1109 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-$CI_PIPELINE_IID.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

@ -2,17 +2,23 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
group = "dev.fyloz.colorrecipesexplorer"
val kotlinVersion = "1.5.0"
val springBootVersion = "2.3.4.RELEASE"
plugins {
// Outer scope variables can't be accessed in the plugins section, so we have to redefine them here
val kotlinVersion = "1.5.0"
val springBootVersion = "2.3.4.RELEASE"
id("java")
id("org.jetbrains.kotlin.jvm") version "1.4.30"
id("org.jetbrains.dokka") version "1.4.20"
id("org.springframework.boot") version "2.3.4.RELEASE"
id("org.jetbrains.kotlin.plugin.spring") version "1.4.30"
id("org.jetbrains.kotlin.plugin.jpa") version "1.4.30"
id("org.jetbrains.kotlin.jvm") version kotlinVersion
id("org.jetbrains.dokka") version "1.4.32"
id("org.springframework.boot") version springBootVersion
id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
}
repositories {
jcenter()
mavenCentral()
maven {
@ -21,38 +27,43 @@ repositories {
}
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.4.10"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.10")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.4.10")
implementation(platform("org.jetbrains.kotlin:kotlin-bom:${kotlinVersion}"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.3")
implementation("javax.xml.bind:jaxb-api:2.3.0")
implementation("io.jsonwebtoken:jjwt:0.9.1")
implementation("org.apache.poi:poi-ooxml:4.1.0")
implementation("org.apache.pdfbox:pdfbox:2.0.4")
implementation("dev.fyloz.colorrecipesexplorer:database-manager:1.2.0")
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.1")
implementation("org.springframework.boot:spring-boot-starter-data-jpa:2.3.4.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-jdbc:2.3.4.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-web:2.3.4.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-validation:2.3.4.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-security:2.3.4.RELEASE")
implementation("org.springframework.boot:spring-boot-configuration-processor:2.3.4.RELEASE")
implementation("org.springframework.boot:spring-boot-devtools:2.3.4.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-validation:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-starter-security:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
testImplementation("org.springframework:spring-test:5.1.6.RELEASE")
testImplementation("org.mockito:mockito-inline:3.6.0")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.2")
testImplementation("io.mockk:mockk:1.10.6")
testImplementation("org.springframework.boot:spring-boot-starter-test:2.3.4.RELEASE")
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:2.3.4.RELEASE")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.4.10")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.4.10")
testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}")
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:${springBootVersion}")
testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
runtimeOnly("com.h2database:h2:1.4.199")
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 {
@ -90,8 +101,8 @@ tasks.withType<KotlinCompile>().all {
jvmTarget = JavaVersion.VERSION_11.toString()
useIR = true
freeCompilerArgs = listOf(
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
"-Xinline-classes"
"-Xopt-in=kotlin.contracts.ExperimentalContracts",
"-Xinline-classes"
)
}
}

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 = 4
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.Employee
import dev.fyloz.colorrecipesexplorer.model.EmployeeLoginRequest
import dev.fyloz.colorrecipesexplorer.model.EmployeePermission
import dev.fyloz.colorrecipesexplorer.service.EmployeeService
import dev.fyloz.colorrecipesexplorer.service.EmployeeServiceImpl
import dev.fyloz.colorrecipesexplorer.service.EmployeeUserDetailsService
import dev.fyloz.colorrecipesexplorer.service.EmployeeUserDetailsServiceImpl
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
@ -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
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,17 +46,19 @@ 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: EmployeeUserDetailsServiceImpl,
@Lazy val employeeService: EmployeeServiceImpl,
val environment: Environment,
val logger: Logger
val securityConfigurationProperties: SecurityConfigurationProperties,
@Lazy val userDetailsService: CreUserDetailsService,
@Lazy val userService: UserService,
val environment: Environment,
val logger: Logger
) : WebSecurityConfigurerAdapter() {
var debugMode = false
@ -66,93 +68,96 @@ 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<EmployeePermission>
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 (!employeeService.existsById(credentials.id!!)) {
employeeService.save(
Employee(
id = credentials.id!!,
firstName = firstName,
lastName = lastName,
password = passwordEncoder().encode(credentials.password!!),
isSystemUser = true,
permissions = permissions.toMutableSet()
)
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(EmployeePermission.ADMIN))
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(),
employeeService,
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/employee/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")
}
@ -171,9 +176,9 @@ const val defaultGroupCookieName = "Default-Group"
val blacklistedJwtTokens = mutableListOf<String>()
class JwtAuthenticationFilter(
private val authManager: AuthenticationManager,
private val employeeService: EmployeeService,
private val securityConfigurationProperties: SecurityConfigurationProperties
private val authManager: AuthenticationManager,
private val securityConfigurationProperties: SecurityConfigurationProperties,
private val updateUserLoginTime: (Long) -> Unit
) : UsernamePasswordAuthenticationFilter() {
private var debugMode = false
@ -183,36 +188,36 @@ class JwtAuthenticationFilter(
}
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, EmployeeLoginRequest::class.java)
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
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 employeeId = (authResult.principal as User).username
employeeService.updateLastLoginTime(employeeId.toLong())
val userId = (authResult.principal as SpringUser).username
updateUserLoginTime(userId.toLong())
val expirationMs = System.currentTimeMillis() + jwtDuration!!
val expirationDate = Date(expirationMs)
val token = Jwts.builder()
.setSubject(employeeId)
.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: EmployeeUserDetailsService,
private val securityConfigurationProperties: SecurityConfigurationProperties,
authenticationManager: AuthenticationManager
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 {
@ -259,20 +264,20 @@ class JwtAuthorizationFilter(
val jwtSecret = securityConfigurationProperties.jwtSecret
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
return try {
val employeeId = Jwts.parser()
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
if (employeeId != null) getAuthenticationToken(employeeId) else null
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(employeeId: String): UsernamePasswordAuthenticationToken? = try {
val employeeDetails = userDetailsService.loadUserByEmployeeId(employeeId.toLong(), false)
UsernamePasswordAuthenticationToken(employeeDetails.username, null, employeeDetails.authorities)
private fun getAuthenticationToken(userId: String): UsernamePasswordAuthenticationToken? = try {
val userDetails = loadUserById(userId.toLong())
UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities)
} catch (_: NotFoundException) {
null
}

View File

@ -14,12 +14,6 @@ annotation class PreAuthorizeViewRecipes
@PreAuthorize("hasAuthority('EDIT_RECIPES')")
annotation class PreAuthorizeEditRecipes
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@PreAuthorize("hasAuthority('REMOVE_RECIPES')")
annotation class PreAuthorizeRemoveRecipes
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@ -37,9 +31,3 @@ annotation class PreAuthorizeViewUsers
@MustBeDocumented
@PreAuthorize("hasAuthority('EDIT_USERS')")
annotation class PreAuthorizeEditUsers
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@PreAuthorize("hasAuthority('REMOVE_USERS')")
annotation class PreAuthorizeRemoveUsers

View File

@ -12,63 +12,62 @@ import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
abstract class RestException(
val errorCode: String,
val title: String,
val status: HttpStatus,
val details: String,
val extensions: Map<String, Any> = mapOf()
val errorCode: String,
val title: String,
val status: HttpStatus,
val details: String,
val extensions: Map<String, Any> = mapOf()
) : RuntimeException(details) {
fun buildExceptionBody() = mapOf(
"type" to errorCode,
"title" to title,
"status" to status.value(),
"detail" to details,
"type" to errorCode,
"title" to title,
"status" to status.value(),
"detail" to details,
*extensions.map { it.key to it.value }.toTypedArray()
*extensions.map { it.key to it.value }.toTypedArray()
)
}
class NotFoundException(
errorCode: String,
title: String,
details: String,
identifierValue: Any,
identifierName: String = "id"
errorCode: String,
title: String,
details: String,
identifierValue: Any,
identifierName: String = "id"
) : RestException(
errorCode = "notfound-$errorCode-$identifierName",
title = title,
status = HttpStatus.NOT_FOUND,
details = details,
extensions = mapOf(
identifierName to identifierValue
)
errorCode = "notfound-$errorCode-$identifierName",
title = title,
status = HttpStatus.NOT_FOUND,
details = details,
extensions = mapOf(
identifierName to identifierValue
)
)
class AlreadyExistsException(
errorCode: String,
title: String,
details: String,
identifierValue: Any,
identifierName: String = "id"
errorCode: String,
title: String,
details: String,
identifierValue: Any,
identifierName: String = "id",
extensions: MutableMap<String, Any> = mutableMapOf()
) : RestException(
errorCode = "exists-$errorCode-$identifierName",
title = title,
status = HttpStatus.CONFLICT,
details = details,
extensions = mapOf(
identifierName to identifierValue
)
errorCode = "exists-$errorCode-$identifierName",
title = title,
status = HttpStatus.CONFLICT,
details = details,
extensions = extensions.apply { this[identifierName] = identifierValue }.toMap()
)
class CannotDeleteException(
errorCode: String,
title: String,
details: String
errorCode: String,
title: String,
details: String
) : RestException(
errorCode = "cannotdelete-$errorCode",
title = title,
status = HttpStatus.CONFLICT,
details = details
errorCode = "cannotdelete-$errorCode",
title = title,
status = HttpStatus.CONFLICT,
details = details
)
@ControllerAdvice
@ -79,19 +78,19 @@ class RestResponseEntityExceptionHandler : ResponseEntityExceptionHandler() {
finalBody["instance"] = (request as ServletWebRequest).request.requestURI
return handleExceptionInternal(
exception,
finalBody,
HttpHeaders(),
exception.status,
request
exception,
finalBody,
HttpHeaders(),
exception.status,
request
)
}
override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
ex: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
): ResponseEntity<Any> {
val errors = hashMapOf<String, String>()
ex.bindingResult.allErrors.forEach {

View File

@ -8,9 +8,6 @@ import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
private const val COMPANY_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val COMPANY_NAME_NULL_MESSAGE = "Un nom est requis"
@Entity
@Table(name = "company")
data class Company(
@ -20,11 +17,15 @@ data class Company(
@Column(unique = true)
override val name: String
) : NamedModel
) : NamedModel {
override fun toString(): String {
return name
}
}
open class CompanySaveDto(
@field:NotBlank(message = COMPANY_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String
) : EntityDto<Company> {
override fun toEntity(): Company = Company(null, name)
@ -32,10 +33,9 @@ open class CompanySaveDto(
open class CompanyUpdateDto(
@field:NotNull(message = COMPANY_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = COMPANY_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?
) : EntityDto<Company> {
override fun toEntity(): Company = Company(id, name ?: "")

View File

@ -0,0 +1,146 @@
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 ConfigurationEntity(
@Id
@Column(name = "config_key")
val key: String,
val content: String,
@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

@ -1,193 +0,0 @@
package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val EMPLOYEE_ID_NULL_MESSAGE = "Un numéro d'employé est requis"
private const val EMPLOYEE_LAST_NAME_EMPTY_MESSAGE = "Un nom est requis"
private const val EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis"
private const val EMPLOYEE_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis"
private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères"
@Entity
@Table(name = "employee")
data class Employee(
@Id
override val id: Long,
@Column(name = "first_name")
val firstName: String = "",
@Column(name = "last_name")
val lastName: String = "",
val password: String = "",
@Column(name = "default_group_user")
val isDefaultGroupUser: Boolean = false,
@Column(name = "system_user")
val isSystemUser: Boolean = false,
@ManyToOne
@JoinColumn(name = "group_id")
@Fetch(FetchMode.SELECT)
var group: EmployeeGroup? = null,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "employee_permission", joinColumns = [JoinColumn(name = "employee_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
@Column(name = "last_login_time")
var lastLoginTime: LocalDateTime? = null
) : Model {
val flatPermissions: Set<EmployeePermission>
get() = permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toMutableSet()
.apply {
if (group != null) this.addAll(group!!.flatPermissions)
}
val authorities: Set<GrantedAuthority>
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
}
/** DTO for creating employees. Allows a [password] a [groupId]. */
open class EmployeeSaveDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String,
@field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String,
@field:NotBlank(message = EMPLOYEE_PASSWORD_EMPTY_MESSAGE)
@field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE)
val password: String,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: MutableSet<EmployeePermission> = mutableSetOf()
) : EntityDto<Employee>
open class EmployeeUpdateDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String?,
@field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String?,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: Set<EmployeePermission>?
) : EntityDto<Employee>
data class EmployeeOutputDto(
override val id: Long,
val firstName: String,
val lastName: String,
val group: EmployeeGroup?,
val permissions: Set<EmployeePermission>,
val explicitPermissions: Set<EmployeePermission>,
val lastLoginTime: LocalDateTime?
) : Model
data class EmployeeLoginRequest(val id: Long, val password: String)
// ==== DSL ====
fun employee(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: EmployeeGroup? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: Employee.() -> Unit = {}
) = Employee(
id,
firstName,
lastName,
password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun employeeSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeSaveDto.() -> Unit = {}
) = EmployeeSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
fun employeeUpdateDto(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeUpdateDto.() -> Unit = {}
) = EmployeeUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
// ==== Exceptions ====
private const val EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE = "Employee not found"
private const val EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE = "Employee already exists"
private const val EMPLOYEE_EXCEPTION_ERROR_CODE = "employee"
fun employeeIdNotFoundException(id: Long) =
NotFoundException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE,
"An employee with the id $id could not be found",
id
)
fun employeeIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee with the id $id already exists",
id
)
fun employeeFullNameAlreadyExistsException(firstName: String, lastName: String) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee with the name '$firstName $lastName' already exists",
"$firstName $lastName",
"fullName"
)

View File

@ -1,141 +0,0 @@
package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonProperty
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.http.HttpStatus
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val GROUP_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val GROUP_NAME_NULL_MESSAGE = "Un nom est requis"
private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est requise"
@Entity
@Table(name = "employee_group")
data class EmployeeGroup(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override var id: Long? = null,
@Column(unique = true)
override val name: String = "",
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
) : NamedModel {
val flatPermissions: Set<EmployeePermission>
get() = this.permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toSet()
}
open class EmployeeGroupSaveDto(
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(null, name, permissions)
}
open class EmployeeGroupUpdateDto(
@field:NotNull(message = GROUP_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(id, name, permissions)
}
data class EmployeeGroupOutputDto(
override val id: Long,
val name: String,
val permissions: Set<EmployeePermission>,
val explicitPermissions: Set<EmployeePermission>
): Model
fun employeeGroup(
id: Long? = null,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroup.() -> Unit = {}
) = EmployeeGroup(id, name, permissions).apply(op)
fun employeeGroupSaveDto(
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupSaveDto.() -> Unit = {}
) = EmployeeGroupSaveDto(name, permissions).apply(op)
fun employeeGroupUpdateDto(
id: Long = 0L,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupUpdateDto.() -> Unit = {}
) = EmployeeGroupUpdateDto(id, name, permissions).apply(op)
// ==== Exceptions ====
private const val EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE = "Employee group not found"
private const val EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE = "Employee group already exists"
private const val EMPLOYEE_EXCEPTION_ERROR_CODE = "employeegroup"
class NoDefaultGroupException : RestException(
"nodefaultgroup",
"No default group",
HttpStatus.NOT_FOUND,
"No default group cookie is defined in the current request"
)
fun employeeGroupIdNotFoundException(id: Long) =
NotFoundException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE,
"An employee group with the id $id could not be found",
id
)
fun employeeGroupNameNotFoundException(name: String) =
NotFoundException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE,
"An employee group with the name $name could not be found",
name,
"name"
)
fun employeeGroupIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee group with the id $id already exists",
id,
)
fun employeeGroupNameAlreadyExistsException(name: String) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee group with the name $name already exists",
name,
"name"
)

View File

@ -4,27 +4,11 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
import org.springframework.web.multipart.MultipartFile
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
private const val MATERIAL_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val MATERIAL_NAME_NULL_MESSAGE = "Un nom est requis"
private const val MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE = "Une quantité est requise"
private const val MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE = "La quantité doit être supérieure ou égale à 0"
private const val MATERIAL_TYPE_NULL_MESSAGE = "Un type de produit est requis"
private const val MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE = "Un produit est requis"
private const val MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE = "Une quantité est requises"
private const val MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE = "La quantité doit être supérieure ou égale à 0"
import javax.validation.constraints.Size
const val SIMDUT_FILES_PATH = "pdf/simdut"
@ -52,32 +36,27 @@ data class Material(
@JsonIgnore
@Transient
get() = "$SIMDUT_FILES_PATH/$name.pdf"
}
open class MaterialSaveDto(
@field:NotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String,
@field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val inventoryQuantity: Float,
@field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long,
val simdutFile: MultipartFile? = null
) : EntityDto<Material>
open class MaterialUpdateDto(
@field:NotNull(message = MATERIAL_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?,
@field:NullOrSize(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val inventoryQuantity: Float?,
val materialTypeId: Long?,
@ -95,11 +74,9 @@ data class MaterialOutputDto(
) : Model
data class MaterialQuantityDto(
@field:NotNull(message = MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE)
val material: Long,
@field:NotNull(message = MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val quantity: Float
)
@ -147,7 +124,7 @@ fun materialQuantityDto(
) = MaterialQuantityDto(materialId, quantity).apply(op)
// ==== Exceptions ====
private const
private const
val MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Material not found"
private const val MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Material already exists"
private const val MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material"

View File

@ -11,10 +11,7 @@ import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val MATERIAL_TYPE_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val MATERIAL_TYPE_NAME_NULL_MESSAGE = "Un nom est requis"
private const val MATERIAL_TYPE_PREFIX_NULL_MESSAGE = "Un préfixe est requis"
private const val MATERIAL_TYPE_PREFIX_SIZE_MESSAGE = "Le préfixe doit faire exactement 3 caractères"
private const val VALIDATION_PREFIX_SIZE = "Must contains exactly 3 characters"
@Entity
@Table(name = "material_type")
@ -39,11 +36,11 @@ data class MaterialType(
) : NamedModel
open class MaterialTypeSaveDto(
@field:NotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String,
@field:NotBlank(message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE)
@field:Size(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_SIZE_MESSAGE)
@field:NotBlank
@field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE)
val prefix: String,
val usePercentages: Boolean = false
@ -53,13 +50,12 @@ open class MaterialTypeSaveDto(
}
open class MaterialTypeUpdateDto(
@field:NotNull(message = MATERIAL_TYPE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?,
@field:NullOrSize(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE)
@field:Size(min = 3, max = 3, message = VALIDATION_PREFIX_SIZE)
val prefix: String?
) : EntityDto<MaterialType> {
override fun toEntity(): MaterialType =

View File

@ -4,20 +4,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
private const val MIX_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val MIX_NAME_NULL_MESSAGE = "Un nom est requis"
private const val MIX_RECIPE_NULL_MESSAGE = "Un recette est requise"
private const val MIX_MATERIAL_TYPE_NULL_MESSAGE = "Un type de produit est requis"
private const val MIX_DEDUCT_MIX_ID_NULL_MESSAGE = "Un identifiant de mélange est requis"
private const val MIX_DEDUCT_RATIO_NULL_MESSAGE = "Un ratio est requis"
private const val MIX_DEDUCT_RATION_NEGATIVE_MESSAGE = "Le ratio doit être égal ou supérieur à 0"
@Entity
@Table(name = "mix")
@ -43,33 +33,26 @@ data class Mix(
) : Model
open class MixSaveDto(
@field:NotBlank(message = MIX_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String,
@field:NotNull(message = MIX_RECIPE_NULL_MESSAGE)
val recipeId: Long,
@field:NotNull(message = MIX_MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long,
val mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix> {
override fun toEntity(): Mix = throw UnsupportedOperationException()
}
) : EntityDto<Mix>
open class MixUpdateDto(
@field:NotNull(message = MIX_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = MIX_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?,
val materialTypeId: Long?,
var mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix> {
override fun toEntity(): Mix = throw UnsupportedOperationException()
}
) : EntityDto<Mix>
data class MixOutputDto(
val id: Long,
@ -79,16 +62,13 @@ data class MixOutputDto(
)
data class MixDeductDto(
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val id: Long,
@field:NotNull(message = MIX_DEDUCT_RATIO_NULL_MESSAGE)
@field:Min(value = 0, message = MIX_DEDUCT_RATION_NEGATIVE_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val ratio: Float
)
data class MixLocationDto(
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val mixId: Long,
val location: String?

View File

@ -6,10 +6,6 @@ import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotNull
private const val MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE = "Un identifiant de produit est requis"
private const val MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE = "Une quantité est requise"
private const val MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE = "La quantité ne peut pas être négative"
@Entity
@Table(name = "mix_material")
data class MixMaterial(
@ -26,6 +22,15 @@ data class MixMaterial(
var position: Int
) : Model
data class MixMaterialDto(
val materialId: Long,
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val quantity: Float,
val position: Int
)
data class MixMaterialOutputDto(
val id: Long,
val material: MaterialOutputDto,
@ -33,17 +38,6 @@ data class MixMaterialOutputDto(
val position: Int
)
data class MixMaterialDto(
@field:NotNull(message = MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE)
val materialId: Long,
@field:NotNull(message = MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE)
val quantity: Float,
val position: Int
)
// ==== DSL ====
fun mixMaterial(
id: Long? = null,

View File

@ -15,3 +15,8 @@ interface EntityDto<out E> {
throw UnsupportedOperationException()
}
}
// GENERAL VALIDATION MESSAGES
const val VALIDATION_SIZE_GE_ZERO = "Must be greater or equals to 0"
const val VALIDATION_SIZE_GE_ONE = "Must be greater or equals to 1"
const val VALIDATION_RANGE_PERCENTS = "Must be between 0 and 100"

View File

@ -3,29 +3,17 @@ package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
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
import javax.persistence.*
import javax.validation.constraints.*
private const val RECIPE_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val RECIPE_NAME_NULL_MESSAGE = "Un nom est requis"
private const val RECIPE_DESCRIPTION_NULL_MESSAGE = "Une description est requise"
private const val RECIPE_COLOR_NULL_MESSAGE = "Une couleur est requise"
private const val RECIPE_GLOSS_NULL_MESSAGE = "Le lustre de la couleur est requis"
private const val RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE = "Le lustre doit être entre 0 et 100"
private const val RECIPE_SAMPLE_TOO_SMALL_MESSAGE = "Le numéro d'échantillon doit être supérieur ou égal à 0"
private const val RECIPE_COMPANY_NULL_MESSAGE = "Une bannière est requise"
private const val RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE = "Un identifiant de groupe est requis"
private const val RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE = "Des messages sont requis"
private const val NOTE_GROUP_ID_NULL_MESSAGE = "Un identifiant de groupe est requis"
private const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$"
const val RECIPE_IMAGES_DIRECTORY = "images/recipes"
@ -79,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
@ -89,30 +77,28 @@ data class Recipe(
}
open class RecipeSaveDto(
@field:NotBlank(message = RECIPE_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String,
@field:NotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
@field:NotBlank
val description: String,
@field:NotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
@field:NotBlank
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
val color: String,
@field:NotNull(message = RECIPE_GLOSS_NULL_MESSAGE)
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Min(0, message = VALIDATION_RANGE_PERCENTS)
@field:Max(100, message = VALIDATION_RANGE_PERCENTS)
val gloss: Byte,
@field:Min(value = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val sample: Int?,
val approbationDate: LocalDate?,
val remark: String?,
@field:Min(value = 0, message = RECIPE_COMPANY_NULL_MESSAGE)
val companyId: Long = -1L,
val companyId: Long
) : EntityDto<Recipe> {
override fun toEntity(): Recipe = recipe(
name = name,
@ -125,24 +111,23 @@ open class RecipeSaveDto(
}
open class RecipeUpdateDto(
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = RECIPE_NAME_NULL_MESSAGE)
@field:NotBlank
val name: String?,
@field:NullOrNotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
@field:NotBlank
val description: String?,
@field:NullOrNotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
@field:NotBlank
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
val color: String?,
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Min(0, message = VALIDATION_RANGE_PERCENTS)
@field:Max(100, message = VALIDATION_RANGE_PERCENTS)
val gloss: Byte?,
@field:NullOrSize(min = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
@field:Min(0, message = VALIDATION_SIZE_GE_ZERO)
val sample: Int?,
val approbationDate: LocalDate?,
@ -176,7 +161,7 @@ data class RecipeGroupInformation(
@ManyToOne
@JoinColumn(name = "group_id")
val group: EmployeeGroup,
val group: Group,
var note: String?,
@ -186,15 +171,12 @@ data class RecipeGroupInformation(
)
data class RecipeStepsDto(
@field:NotNull(message = RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE)
val groupId: Long,
@field:NotNull(message = RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE)
val steps: Set<RecipeStep>
)
data class RecipePublicDataDto(
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val recipeId: Long,
val notes: Set<NoteDto>?,
@ -203,7 +185,6 @@ data class RecipePublicDataDto(
)
data class NoteDto(
@field:NotNull(message = NOTE_GROUP_ID_NULL_MESSAGE)
val groupId: Long,
val content: String?
@ -264,7 +245,7 @@ fun recipeUpdateDto(
fun recipeGroupInformation(
id: Long? = null,
group: EmployeeGroup = employeeGroup(),
group: Group = group(),
note: String? = null,
steps: MutableSet<RecipeStep>? = mutableSetOf(),
op: RecipeGroupInformation.() -> Unit = {}
@ -303,3 +284,16 @@ fun recipeIdAlreadyExistsException(id: Long) =
"A recipe with the id $id already exists",
id
)
fun recipeNameAlreadyExistsForCompanyException(name: String, company: Company) =
AlreadyExistsException(
"${RECIPE_EXCEPTION_ERROR_CODE}-company",
RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE,
"A recipe with the name $name already exists for the company ${company.name}",
name,
"name",
mutableMapOf(
"company" to company.name,
"companyId" to company.id!!
)
)

View File

@ -0,0 +1,135 @@
package dev.fyloz.colorrecipesexplorer.model.account
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.*
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.http.HttpStatus
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
@Entity
@Table(name = "user_group")
data class Group(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override var id: Long? = null,
@Column(unique = true)
override val name: String = "",
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<Permission> = mutableSetOf(),
) : NamedModel {
val flatPermissions: Set<Permission>
get() = this.permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toSet()
}
open class GroupSaveDto(
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =
Group(null, name, permissions)
}
open class GroupUpdateDto(
val id: Long,
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =
Group(id, name, permissions)
}
data class GroupOutputDto(
override val id: Long,
val name: String,
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>
): Model
fun group(
id: Long? = null,
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: Group.() -> Unit = {}
) = Group(id, name, permissions).apply(op)
fun groupSaveDto(
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: GroupSaveDto.() -> Unit = {}
) = GroupSaveDto(name, permissions).apply(op)
fun groupUpdateDto(
id: Long = 0L,
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: GroupUpdateDto.() -> Unit = {}
) = GroupUpdateDto(id, name, permissions).apply(op)
// ==== Exceptions ====
private const val GROUP_NOT_FOUND_EXCEPTION_TITLE = "Group not found"
private const val GROUP_ALREADY_EXISTS_EXCEPTION_TITLE = "Group already exists"
private const val GROUP_EXCEPTION_ERROR_CODE = "group"
class NoDefaultGroupException : RestException(
"nodefaultgroup",
"No default group",
HttpStatus.NOT_FOUND,
"No default group cookie is defined in the current request"
)
fun groupIdNotFoundException(id: Long) =
NotFoundException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_NOT_FOUND_EXCEPTION_TITLE,
"A group with the id $id could not be found",
id
)
fun groupNameNotFoundException(name: String) =
NotFoundException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_NOT_FOUND_EXCEPTION_TITLE,
"A group with the name $name could not be found",
name,
"name"
)
fun groupIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
"A group with the id $id already exists",
id,
)
fun groupNameAlreadyExistsException(name: String) =
AlreadyExistsException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
"A group with the name $name already exists",
name,
"name"
)

View File

@ -1,22 +1,19 @@
package dev.fyloz.colorrecipesexplorer.model
package dev.fyloz.colorrecipesexplorer.model.account
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
enum class EmployeePermission(
val impliedPermissions: List<EmployeePermission> = listOf(),
enum class Permission(
val impliedPermissions: List<Permission> = listOf(),
val deprecated: Boolean = false
) {
READ_FILE,
WRITE_FILE(listOf(READ_FILE)),
REMOVE_FILE(listOf(WRITE_FILE)),
VIEW_RECIPES(listOf(READ_FILE)),
VIEW_CATALOG(listOf(READ_FILE)),
VIEW_USERS,
PRINT_MIXES(listOf(VIEW_RECIPES)),
EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)),
EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA, WRITE_FILE)),
EDIT_MATERIALS(listOf(VIEW_CATALOG, WRITE_FILE)),
@ -25,29 +22,24 @@ enum class EmployeePermission(
EDIT_USERS(listOf(VIEW_USERS)),
EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)),
REMOVE_RECIPES(listOf(EDIT_RECIPES, REMOVE_FILE)),
REMOVE_MATERIALS(listOf(EDIT_MATERIALS, REMOVE_FILE)),
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES)),
REMOVE_COMPANIES(listOf(EDIT_COMPANIES)),
REMOVE_USERS(listOf(EDIT_USERS)),
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES)),
VIEW_TOUCH_UP_KITS,
EDIT_TOUCH_UP_KITS(listOf(VIEW_TOUCH_UP_KITS)),
PRINT_MIXES(listOf(VIEW_RECIPES)),
ADD_TO_INVENTORY(listOf(VIEW_CATALOG)),
DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)),
GENERATE_TOUCH_UP_KIT,
ADMIN(
listOf(
EDIT_RECIPES,
EDIT_CATALOG,
EDIT_USERS,
REMOVE_RECIPES,
REMOVE_USERS,
REMOVE_CATALOG,
EDIT_TOUCH_UP_KITS,
PRINT_MIXES,
ADD_TO_INVENTORY,
DEDUCT_FROM_INVENTORY,
GENERATE_TOUCH_UP_KIT
)
),
@ -69,6 +61,16 @@ enum class EmployeePermission(
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true),
EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true),
REMOVE_FILE(listOf(WRITE_FILE), true),
GENERATE_TOUCH_UP_KIT(listOf(VIEW_TOUCH_UP_KITS), true),
REMOVE_RECIPES(listOf(EDIT_RECIPES, REMOVE_FILE), true),
REMOVE_MATERIALS(listOf(EDIT_MATERIALS, REMOVE_FILE), true),
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES), true),
REMOVE_COMPANIES(listOf(EDIT_COMPANIES), true),
REMOVE_USERS(listOf(EDIT_USERS), true),
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES), true),
REMOVE_RECIPE(listOf(REMOVE_RECIPES), true),
REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true),
REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true),
@ -80,12 +82,12 @@ enum class EmployeePermission(
SET_BROWSER_DEFAULT_GROUP(listOf(VIEW_USERS), true),
;
operator fun contains(permission: EmployeePermission): Boolean {
operator fun contains(permission: Permission): Boolean {
return permission == this || impliedPermissions.any { permission in it }
}
}
fun EmployeePermission.flat(): Iterable<EmployeePermission> {
fun Permission.flat(): Iterable<Permission> {
return mutableSetOf(this).apply {
impliedPermissions.forEach {
addAll(it.flat())
@ -93,7 +95,7 @@ fun EmployeePermission.flat(): Iterable<EmployeePermission> {
}
}
/** Converts the given [EmployeePermission] to a [GrantedAuthority]. */
fun EmployeePermission.toAuthority(): GrantedAuthority {
/** Converts the given [Permission] to a [GrantedAuthority]. */
fun Permission.toAuthority(): GrantedAuthority {
return SimpleGrantedAuthority(name)
}

View File

@ -0,0 +1,186 @@
package dev.fyloz.colorrecipesexplorer.model.account
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.Model
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size
private const val VALIDATION_PASSWORD_LENGTH = "Must contains at least 8 characters"
@Entity
@Table(name = "user")
data class User(
@Id
override val id: Long,
@Column(name = "first_name")
val firstName: String = "",
@Column(name = "last_name")
val lastName: String = "",
val password: String = "",
@Column(name = "default_group_user")
val isDefaultGroupUser: Boolean = false,
@Column(name = "system_user")
val isSystemUser: Boolean = false,
@ManyToOne
@JoinColumn(name = "group_id")
@Fetch(FetchMode.SELECT)
var group: Group? = null,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_permission", joinColumns = [JoinColumn(name = "user_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<Permission> = mutableSetOf(),
@Column(name = "last_login_time")
var lastLoginTime: LocalDateTime? = null
) : Model {
val flatPermissions: Set<Permission>
get() = permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toMutableSet()
.apply {
if (group != null) this.addAll(group!!.flatPermissions)
}
val authorities: Set<GrantedAuthority>
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
}
open class UserSaveDto(
val id: Long,
@field:NotBlank
val firstName: String,
@field:NotBlank
val lastName: String,
@field:NotBlank
@field:Size(min = 8, message = VALIDATION_PASSWORD_LENGTH)
val password: String,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: MutableSet<Permission> = mutableSetOf()
) : EntityDto<User>
open class UserUpdateDto(
val id: Long,
@field:NotBlank
val firstName: String?,
@field:NotBlank
val lastName: String?,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: Set<Permission>?
) : EntityDto<User>
data class UserOutputDto(
override val id: Long,
val firstName: String,
val lastName: String,
val group: Group?,
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>,
val lastLoginTime: LocalDateTime?
) : Model
data class UserLoginRequest(val id: Long, val password: String)
// ==== DSL ====
fun user(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: Group? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: User.() -> Unit = {}
) = User(
id,
firstName,
lastName,
password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun userSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
op: UserSaveDto.() -> Unit = {}
) = UserSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
fun userUpdateDto(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
groupId: Long? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
op: UserUpdateDto.() -> Unit = {}
) = UserUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
// ==== Exceptions ====
private const val USER_NOT_FOUND_EXCEPTION_TITLE = "User not found"
private const val USER_ALREADY_EXISTS_EXCEPTION_TITLE = "User already exists"
private const val USER_EXCEPTION_ERROR_CODE = "user"
fun userIdNotFoundException(id: Long) =
NotFoundException(
USER_EXCEPTION_ERROR_CODE,
USER_NOT_FOUND_EXCEPTION_TITLE,
"An user with the id $id could not be found",
id
)
fun userIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
USER_EXCEPTION_ERROR_CODE,
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
"An user with the id $id already exists",
id
)
fun userFullNameAlreadyExistsException(firstName: String, lastName: String) =
AlreadyExistsException(
USER_EXCEPTION_ERROR_CODE,
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
"An user with the name '$firstName $lastName' already exists",
"$firstName $lastName",
"fullName"
)

View File

@ -0,0 +1,211 @@
package dev.fyloz.colorrecipesexplorer.model.touchupkit
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.Model
import dev.fyloz.colorrecipesexplorer.model.VALIDATION_SIZE_GE_ONE
import java.time.LocalDate
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
const val TOUCH_UP_KIT_DELIMITER = ';'
@Entity
@Table(name = "touch_up_kit")
data class TouchUpKit(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
val project: String,
val buggy: String,
val company: String,
val quantity: Int,
@Column(name = "shipping_date")
val shippingDate: LocalDate,
@Column(name = "finish")
private val finishConcatenated: String,
@Column(name = "material")
private val materialConcatenated: String,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "touch_up_kit_id")
val content: Set<TouchUpKitProduct>
) : Model {
val finish
get() = finishConcatenated.split(TOUCH_UP_KIT_DELIMITER)
val material
get() = materialConcatenated.split(TOUCH_UP_KIT_DELIMITER)
}
@Entity
@Table(name = "touch_up_kit_product")
data class TouchUpKitProduct(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
val name: String,
val description: String?,
val quantity: Float
) : Model
data class TouchUpKitSaveDto(
@field:NotBlank
val project: String,
@field:NotBlank
val buggy: String,
@field:NotBlank
val company: String,
@field:Min(1, message = VALIDATION_SIZE_GE_ONE)
val quantity: Int,
val shippingDate: LocalDate,
@field:NotEmpty
val finish: List<String>,
@field:NotEmpty
val material: List<String>,
@field:NotEmpty
val content: Set<TouchUpKitProductDto>
) : EntityDto<TouchUpKit> {
override fun toEntity() = touchUpKit(this)
}
data class TouchUpKitUpdateDto(
val id: Long,
@field:NotBlank
val project: String?,
@field:NotBlank
val buggy: String?,
@field:NotBlank
val company: String?,
@field:Min(1, message = VALIDATION_SIZE_GE_ONE)
val quantity: Int?,
val shippingDate: LocalDate?,
@field:NotEmpty
val finish: List<String>?,
@field:NotEmpty
val material: List<String>?,
@field:NotEmpty
val content: Set<TouchUpKitProductDto>?
) : EntityDto<TouchUpKit>
data class TouchUpKitOutputDto(
override val id: Long,
val project: String,
val buggy: String,
val company: String,
val quantity: Int,
val shippingDate: LocalDate,
val finish: List<String>,
val material: List<String>,
val content: Set<TouchUpKitProduct>,
val pdfUrl: String
) : Model
data class TouchUpKitProductDto(
val name: String,
val description: String?,
val quantity: Float
)
// ==== DSL ====
fun touchUpKit(
id: Long? = null,
project: String = "project",
buggy: String = "buggy",
company: String = "company",
quantity: Int = 1,
shippingDate: LocalDate = LocalDate.now(),
finish: List<String>,
material: List<String>,
content: Set<TouchUpKitProduct>,
op: TouchUpKit.() -> Unit = {}
) = TouchUpKit(
id,
project,
buggy,
company,
quantity,
shippingDate,
finish.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" },
material.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" },
content
).apply(op)
fun touchUpKit(touchUpKitSaveDto: TouchUpKitSaveDto) =
with(touchUpKitSaveDto) {
touchUpKit(
project = project,
buggy = buggy,
company = company,
quantity = quantity,
shippingDate = shippingDate,
finish = finish,
material = material,
content = content.map { touchUpKitProduct(it) }.toSet()
)
}
fun touchUpKitProduct(
id: Long? = null,
name: String = "product",
description: String? = "description",
quantity: Float = 1f,
op: TouchUpKitProduct.() -> Unit = {}
) = TouchUpKitProduct(id, name, description, quantity)
.apply(op)
fun touchUpKitProduct(touchUpKitProductDto: TouchUpKitProductDto) =
touchUpKitProduct(
name = touchUpKitProductDto.name,
description = touchUpKitProductDto.description,
quantity = touchUpKitProductDto.quantity
)
// ==== Exceptions ====
private const val TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE = "Touch up kit not found"
private const val TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE = "Touch up kit already exists"
private const val TOUCH_UP_KIT_EXCEPTION_ERROR_CODE = "touchupkit"
fun touchUpKitIdNotFoundException(id: Long) =
NotFoundException(
TOUCH_UP_KIT_EXCEPTION_ERROR_CODE,
TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE,
"A touch up kit with the id $id could not be found",
id
)
fun touchUpKitIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
TOUCH_UP_KIT_EXCEPTION_ERROR_CODE,
TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE,
"A touch up kit with the id $id already exists",
id
)

View File

@ -1,20 +1,20 @@
package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.Employee
import dev.fyloz.colorrecipesexplorer.model.EmployeeGroup
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.account.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface EmployeeRepository : JpaRepository<Employee, Long> {
interface UserRepository : JpaRepository<User, Long> {
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
fun findByFirstNameAndLastName(firstName: String, lastName: String): Employee?
fun findByFirstNameAndLastName(firstName: String, lastName: String): User?
fun findAllByGroup(group: EmployeeGroup): Collection<Employee>
fun findAllByGroup(group: Group): Collection<User>
fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: EmployeeGroup): Employee
fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: Group): User
}
@Repository
interface EmployeeGroupRepository : NamedJpaRepository<EmployeeGroup>
interface GroupRepository : NamedJpaRepository<Group>

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

@ -8,6 +8,12 @@ interface RecipeRepository : JpaRepository<Recipe, Long> {
/** Checks if one or more recipes have the given [company]. */
fun existsByCompany(company: Company): Boolean
/** Checks if a recipe exists with the given [name] and [company]. */
fun existsByNameAndCompany(name: String, company: Company): Boolean
/** Gets all recipes with the given [name]. */
fun findAllByName(name: String): Collection<Recipe>
/** Gets all recipes with the given [company]. */
fun findAllByCompany(company: Company): Collection<Recipe>
}

View File

@ -0,0 +1,6 @@
package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKit
import org.springframework.data.jpa.repository.JpaRepository
interface TouchUpKitRepository : JpaRepository<TouchUpKit, Long>

View File

@ -1,11 +1,11 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveUsers
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.service.EmployeeGroupService
import dev.fyloz.colorrecipesexplorer.service.EmployeeService
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.*
@ -14,29 +14,30 @@ import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.validation.Valid
private const val EMPLOYEE_CONTROLLER_PATH = "api/employee"
private const val EMPLOYEE_GROUP_CONTROLLER_PATH = "api/employee/group"
private const val USER_CONTROLLER_PATH = "api/user"
private const val GROUP_CONTROLLER_PATH = "api/user/group"
@RestController
@RequestMapping(EMPLOYEE_CONTROLLER_PATH)
class EmployeeController(private val employeeService: EmployeeService) {
@RequestMapping(USER_CONTROLLER_PATH)
@Profile("!emergency")
class UserController(private val userService: UserService) {
@GetMapping
@PreAuthorizeViewUsers
fun getAll() =
ok(employeeService.getAllForOutput())
ok(userService.getAllForOutput())
@GetMapping("{id}")
@PreAuthorizeViewUsers
fun getById(@PathVariable id: Long) =
ok(employeeService.getByIdForOutput(id))
ok(userService.getByIdForOutput(id))
@GetMapping("current")
fun getCurrent(loggedInEmployee: Principal?) =
if (loggedInEmployee != null)
fun getCurrent(loggedInUser: Principal?) =
if (loggedInUser != null)
ok(
with(employeeService) {
with(userService) {
getById(
loggedInEmployee.name.toLong(),
loggedInUser.name.toLong(),
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = false
).toOutput()
@ -47,56 +48,57 @@ class EmployeeController(private val employeeService: EmployeeService) {
@PostMapping
@PreAuthorizeEditUsers
fun save(@Valid @RequestBody employee: EmployeeSaveDto) =
created<EmployeeOutputDto>(EMPLOYEE_CONTROLLER_PATH) {
with(employeeService) {
save(employee).toOutput()
fun save(@Valid @RequestBody user: UserSaveDto) =
created<UserOutputDto>(USER_CONTROLLER_PATH) {
with(userService) {
save(user).toOutput()
}
}
@PutMapping
@PreAuthorizeEditUsers
fun update(@Valid @RequestBody employee: EmployeeUpdateDto) =
fun update(@Valid @RequestBody user: UserUpdateDto) =
noContent {
employeeService.update(employee)
userService.update(user)
}
@PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE])
@PreAuthorizeEditUsers
fun updatePassword(@PathVariable id: Long, @RequestBody password: String) =
noContent {
employeeService.updatePassword(id, password)
userService.updatePassword(id, password)
}
@PutMapping("{employeeId}/permissions/{permission}")
@PutMapping("{userId}/permissions/{permission}")
@PreAuthorizeEditUsers
fun addPermission(
@PathVariable employeeId: Long,
@PathVariable permission: EmployeePermission
@PathVariable userId: Long,
@PathVariable permission: Permission
) = noContent {
employeeService.addPermission(employeeId, permission)
userService.addPermission(userId, permission)
}
@DeleteMapping("{employeeId}/permissions/{permission}")
@DeleteMapping("{userId}/permissions/{permission}")
@PreAuthorizeEditUsers
fun removePermission(
@PathVariable employeeId: Long,
@PathVariable permission: EmployeePermission
@PathVariable userId: Long,
@PathVariable permission: Permission
) = noContent {
employeeService.removePermission(employeeId, permission)
userService.removePermission(userId, permission)
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveUsers
@PreAuthorizeEditUsers
fun deleteById(@PathVariable id: Long) =
employeeService.deleteById(id)
userService.deleteById(id)
}
@RestController
@RequestMapping(EMPLOYEE_GROUP_CONTROLLER_PATH)
@RequestMapping(GROUP_CONTROLLER_PATH)
@Profile("!emergency")
class GroupsController(
private val groupService: EmployeeGroupService,
private val employeeService: EmployeeService
private val groupService: GroupService,
private val userService: UserService
) {
@GetMapping
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
@ -108,11 +110,11 @@ class GroupsController(
fun getById(@PathVariable id: Long) =
ok(groupService.getByIdForOutput(id))
@GetMapping("{id}/employees")
@GetMapping("{id}/users")
@PreAuthorizeViewUsers
fun getEmployeesForGroup(@PathVariable id: Long) =
ok(with(employeeService) {
groupService.getEmployeesForGroup(id)
fun getUsersForGroup(@PathVariable id: Long) =
ok(with(userService) {
groupService.getUsersForGroup(id)
.map { it.toOutput() }
})
@ -132,8 +134,8 @@ class GroupsController(
@PostMapping
@PreAuthorizeEditUsers
fun save(@Valid @RequestBody group: EmployeeGroupSaveDto) =
created<EmployeeGroupOutputDto>(EMPLOYEE_GROUP_CONTROLLER_PATH) {
fun save(@Valid @RequestBody group: GroupSaveDto) =
created<GroupOutputDto>(GROUP_CONTROLLER_PATH) {
with(groupService) {
save(group).toOutput()
}
@ -141,13 +143,13 @@ class GroupsController(
@PutMapping
@PreAuthorizeEditUsers
fun update(@Valid @RequestBody group: EmployeeGroupUpdateDto) =
fun update(@Valid @RequestBody group: GroupUpdateDto) =
noContent {
groupService.update(group)
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveUsers
@PreAuthorizeEditUsers
fun deleteById(@PathVariable id: Long) =
noContent {
groupService.deleteById(id)
@ -156,10 +158,11 @@ class GroupsController(
@RestController
@RequestMapping("api")
class LogoutController(private val employeeService: EmployeeService) {
@Profile("!emergency")
class LogoutController(private val userService: UserService) {
@GetMapping("logout")
fun logout(request: HttpServletRequest) =
ok<Void> {
employeeService.logout(request)
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
@ -38,7 +40,7 @@ class CompanyController(private val companyService: CompanyService) {
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_COMPANIES')")
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
fun deleteById(@PathVariable id: Long) =
noContent {
companyService.deleteById(id)

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?
@ -46,7 +45,7 @@ class FileController(
}
@DeleteMapping
@PreAuthorize("hasAnyAuthority('REMOVE_FILE')")
@PreAuthorize("hasAnyAuthority('WRITE_FILE')")
fun delete(@RequestParam path: String): ResponseEntity<Void> {
return noContent {
fileService.delete(path)
@ -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
@ -64,7 +66,7 @@ class MaterialController(
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_MATERIALS')")
@PreAuthorize("hasAuthority('EDIT_MATERIALS')")
fun deleteById(@PathVariable id: Long) =
noContent {
materialService.deleteById(id)

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
@ -38,7 +40,7 @@ class MaterialTypeController(private val materialTypeService: MaterialTypeServic
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_MATERIAL_TYPES')")
@PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')")
fun deleteById(@PathVariable id: Long) =
noContent {
materialTypeService.deleteById(id)

View File

@ -1,21 +1,17 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveRecipes
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
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
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import javax.validation.Valid
@ -24,14 +20,20 @@ private const val MIX_CONTROLLER_PATH = "api/recipe/mix"
@RestController
@RequestMapping(RECIPE_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorizeViewRecipes
class RecipeController(
private val recipeService: RecipeService,
private val recipeImageService: RecipeImageService
) {
@GetMapping
fun getAll() =
ok(recipeService.getAllForOutput())
fun getAll(@RequestParam(required = false) name: String?) =
if (name == null)
ok(recipeService.getAllForOutput())
else
ok(with(recipeService) {
getAllByName(name).map { it.toOutput() }
})
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
@ -61,7 +63,7 @@ class RecipeController(
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveRecipes
@PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
recipeService.deleteById(id)
@ -84,6 +86,7 @@ class RecipeController(
@RestController
@RequestMapping(MIX_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorizeViewRecipes
class MixController(private val mixService: MixService) {
@GetMapping("{id}")
@ -105,7 +108,7 @@ class MixController(private val mixService: MixService) {
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveRecipes
@PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
mixService.deleteById(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

@ -0,0 +1,65 @@
package dev.fyloz.colorrecipesexplorer.rest
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
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
import javax.validation.Valid
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
) {
@GetMapping
fun getAll() =
ok(touchUpKitService.getAllForOutput())
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(touchUpKitService.getByIdForOutput(id))
@PostMapping
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun save(@Valid @RequestBody touchUpKit: TouchUpKitSaveDto) =
created<TouchUpKitOutputDto>(TOUCH_UP_KIT_CONTROLLER_PATH) {
with(touchUpKitService) {
save(touchUpKit).toOutput()
}
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun update(@Valid @RequestBody touchUpKit: TouchUpKitUpdateDto) =
noContent {
touchUpKitService.update(touchUpKit)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun deleteById(@PathVariable id: Long) =
noContent {
touchUpKitService.deleteById(id)
}
@GetMapping("pdf")
fun getJobPdf(@RequestParam project: String): ResponseEntity<ByteArrayResource> {
with(touchUpKitService.generateJobPdfResource(project)) {
return ResponseEntity.ok()
.header("Content-Disposition", "filename=TouchUpKit_$project.pdf")
.contentLength(this.contentLength())
.contentType(MediaType.APPLICATION_PDF)
.body(this)
}
}
}

View File

@ -1,26 +0,0 @@
package dev.fyloz.colorrecipesexplorer.rest.files
import dev.fyloz.colorrecipesexplorer.service.files.TouchUpKitService
import org.springframework.core.io.ByteArrayResource
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/touchup")
@PreAuthorize("hasAuthority('GENERATE_TOUCH_UP_KIT')")
class TouchUpKitController(
private val touchUpKitService: TouchUpKitService
) {
@GetMapping
fun getJobPdf(@RequestParam job: String): ResponseEntity<ByteArrayResource> {
with(touchUpKitService.generateJobPdfResource(job)) {
return ResponseEntity.ok()
.header("Content-Disposition", "filename=TouchUpKit_$job.pdf")
.contentLength(this.contentLength())
.contentType(MediaType.APPLICATION_PDF)
.body(this)
}
}
}

View File

@ -3,12 +3,12 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.blacklistedJwtTokens
import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.model.validation.or
import dev.fyloz.colorrecipesexplorer.repository.EmployeeGroupRepository
import dev.fyloz.colorrecipesexplorer.repository.EmployeeRepository
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import org.springframework.context.annotation.Lazy
import org.springframework.security.core.userdetails.User
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
@ -19,73 +19,75 @@ import java.time.LocalDateTime
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.transaction.Transactional
import org.springframework.security.core.userdetails.User as SpringUser
interface EmployeeService :
ExternalModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeOutputDto, EmployeeRepository> {
/** Check if an [Employee] with the given [firstName] and [lastName] exists. */
interface UserService :
ExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository> {
/** Check if an [User] with the given [firstName] and [lastName] exists. */
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
/** Gets the employee with the given [id]. */
fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee
/** Gets the user with the given [id]. */
fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User
/** Gets all employees which have the given [group]. */
fun getByGroup(group: EmployeeGroup): Collection<Employee>
/** Gets all users which have the given [group]. */
fun getByGroup(group: Group): Collection<User>
/** Gets the default user of the given [group]. */
fun getDefaultGroupEmployee(group: EmployeeGroup): Employee
fun getDefaultGroupUser(group: Group): User
/** Save a default group employee for the given [group]. */
fun saveDefaultGroupEmployee(group: EmployeeGroup)
/** Save a default group user for the given [group]. */
fun saveDefaultGroupUser(group: Group)
/** Updates de given [entity]. **/
fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee
fun update(entity: User, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User
/** Updates the last login time of the employee with the given [employeeId]. */
fun updateLastLoginTime(employeeId: Long, time: LocalDateTime = LocalDateTime.now()): Employee
/** Updates the last login time of the user with the given [userId]. */
fun updateLastLoginTime(userId: Long, time: LocalDateTime = LocalDateTime.now()): User
/** Updates the password of the employee with the given [id]. */
fun updatePassword(id: Long, password: String): Employee
/** Updates the password of the user with the given [id]. */
fun updatePassword(id: Long, password: String): User
/** Adds the given [permission] to the employee with the given [employeeId]. */
fun addPermission(employeeId: Long, permission: EmployeePermission): Employee
/** Adds the given [permission] to the user with the given [userId]. */
fun addPermission(userId: Long, permission: Permission): User
/** Removes the given [permission] from the employee with the given [employeeId]. */
fun removePermission(employeeId: Long, permission: EmployeePermission): Employee
/** Removes the given [permission] from the user with the given [userId]. */
fun removePermission(userId: Long, permission: Permission): User
/** Logout an user. Add the authorization token of the given [request] to the blacklisted tokens. */
fun logout(request: HttpServletRequest)
}
interface EmployeeGroupService :
ExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupOutputDto, EmployeeGroupRepository> {
/** Gets all the employees of the group with the given [id]. */
fun getEmployeesForGroup(id: Long): Collection<Employee>
interface GroupService :
ExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository> {
/** Gets all the users of the group with the given [id]. */
fun getUsersForGroup(id: Long): Collection<User>
/** Gets the default group from a cookie in the given HTTP [request]. */
fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup
fun getRequestDefaultGroup(request: HttpServletRequest): Group
/** Sets the default group cookie for the given HTTP [response]. */
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)
}
interface EmployeeUserDetailsService : UserDetailsService {
/** Loads an [User] for the given [employeeId]. */
fun loadUserByEmployeeId(employeeId: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
interface CreUserDetailsService : UserDetailsService {
/** Loads an [User] for the given [id]. */
fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
}
@Service
class EmployeeServiceImpl(
employeeRepository: EmployeeRepository,
@Lazy val groupService: EmployeeGroupService,
@Profile("!emergency")
class UserServiceImpl(
userRepository: UserRepository,
@Lazy val groupService: GroupService,
@Lazy val passwordEncoder: PasswordEncoder,
) : AbstractExternalModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeOutputDto, EmployeeRepository>(
employeeRepository
) : AbstractExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository>(
userRepository
),
EmployeeService {
override fun idNotFoundException(id: Long) = employeeIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = employeeIdAlreadyExistsException(id)
UserService {
override fun idNotFoundException(id: Long) = userIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = userIdAlreadyExistsException(id)
override fun Employee.toOutput() = EmployeeOutputDto(
override fun User.toOutput() = UserOutputDto(
this.id,
this.firstName,
this.lastName,
@ -98,29 +100,29 @@ class EmployeeServiceImpl(
override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean =
repository.existsByFirstNameAndLastName(firstName, lastName)
override fun getAll(): Collection<Employee> =
override fun getAll(): Collection<User> =
super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser }
override fun getById(id: Long): Employee =
override fun getById(id: Long): User =
getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee =
override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User =
super.getById(id).apply {
if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser)
throw idNotFoundException(id)
}
override fun getByGroup(group: EmployeeGroup): Collection<Employee> =
override fun getByGroup(group: Group): Collection<User> =
repository.findAllByGroup(group).filter {
!it.isSystemUser && !it.isDefaultGroupUser
}
override fun getDefaultGroupEmployee(group: EmployeeGroup): Employee =
override fun getDefaultGroupUser(group: Group): User =
repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)
override fun save(entity: EmployeeSaveDto): Employee =
override fun save(entity: UserSaveDto): User =
save(with(entity) {
Employee(
User(
id,
firstName,
lastName,
@ -132,20 +134,20 @@ class EmployeeServiceImpl(
)
})
override fun save(entity: Employee): Employee {
override fun save(entity: User): User {
if (existsById(entity.id))
throw employeeIdAlreadyExistsException(entity.id)
throw userIdAlreadyExistsException(entity.id)
if (existsByFirstNameAndLastName(entity.firstName, entity.lastName))
throw employeeFullNameAlreadyExistsException(entity.firstName, entity.lastName)
throw userFullNameAlreadyExistsException(entity.firstName, entity.lastName)
return super<AbstractExternalModelService>.save(entity)
}
override fun saveDefaultGroupEmployee(group: EmployeeGroup) {
override fun saveDefaultGroupUser(group: Group) {
save(
employee(
user(
id = 1000000L + group.id!!,
firstName = group.name,
lastName = "EmployeeModel",
lastName = "User",
password = passwordEncoder.encode(group.name),
group = group,
isDefaultGroupUser = true
@ -153,49 +155,49 @@ class EmployeeServiceImpl(
)
}
override fun updateLastLoginTime(employeeId: Long, time: LocalDateTime): Employee {
val employee = getById(employeeId, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false)
employee.lastLoginTime = time
override fun updateLastLoginTime(userId: Long, time: LocalDateTime): User {
val user = getById(userId, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false)
user.lastLoginTime = time
return update(
employee,
user,
ignoreDefaultGroupUsers = true,
ignoreSystemUsers = false
)
}
override fun update(entity: EmployeeUpdateDto): Employee {
val persistedEmployee by lazy { getById(entity.id) }
override fun update(entity: UserUpdateDto): User {
val persistedUser by lazy { getById(entity.id) }
return update(with(entity) {
Employee(
User(
id = id,
firstName = firstName or persistedEmployee.firstName,
lastName = lastName or persistedEmployee.lastName,
password = persistedEmployee.password,
firstName = firstName or persistedUser.firstName,
lastName = lastName or persistedUser.lastName,
password = persistedUser.password,
isDefaultGroupUser = false,
isSystemUser = false,
group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedEmployee.group,
permissions = permissions?.toMutableSet() ?: persistedEmployee.permissions,
lastLoginTime = persistedEmployee.lastLoginTime
group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedUser.group,
permissions = permissions?.toMutableSet() ?: persistedUser.permissions,
lastLoginTime = persistedUser.lastLoginTime
)
})
}
override fun update(entity: Employee): Employee =
override fun update(entity: User): User =
update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
override fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee {
override fun update(entity: User, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User {
with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
if (this != null && id != entity.id)
throw employeeFullNameAlreadyExistsException(entity.firstName, entity.lastName)
throw userFullNameAlreadyExistsException(entity.firstName, entity.lastName)
}
return super<AbstractExternalModelService>.update(entity)
return super.update(entity)
}
override fun updatePassword(id: Long, password: String): Employee {
val persistedEmployee = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
return super<AbstractExternalModelService>.update(with(persistedEmployee) {
Employee(
override fun updatePassword(id: Long, password: String): User {
val persistedUser = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
return super.update(with(persistedUser) {
User(
id,
firstName,
lastName,
@ -209,11 +211,11 @@ class EmployeeServiceImpl(
})
}
override fun addPermission(employeeId: Long, permission: EmployeePermission): Employee =
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions += permission })
override fun addPermission(userId: Long, permission: Permission): User =
super.update(getById(userId).apply { permissions += permission })
override fun removePermission(employeeId: Long, permission: EmployeePermission): Employee =
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions -= permission })
override fun removePermission(userId: Long, permission: Permission): User =
super.update(getById(userId).apply { permissions -= permission })
override fun logout(request: HttpServletRequest) {
val authorizationCookie = WebUtils.getCookie(request, "Authorization")
@ -229,19 +231,20 @@ class EmployeeServiceImpl(
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
@Service
class EmployeeGroupServiceImpl(
private val employeeService: EmployeeService,
employeeGroupRepository: EmployeeGroupRepository
) : AbstractExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupOutputDto, EmployeeGroupRepository>(
employeeGroupRepository
@Profile("!emergency")
class GroupServiceImpl(
private val userService: UserService,
groupRepository: GroupRepository
) : AbstractExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository>(
groupRepository
),
EmployeeGroupService {
override fun idNotFoundException(id: Long) = employeeGroupIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = employeeGroupIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = employeeGroupNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = employeeGroupNameAlreadyExistsException(name)
GroupService {
override fun idNotFoundException(id: Long) = groupIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = groupNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = groupNameAlreadyExistsException(name)
override fun EmployeeGroup.toOutput() = EmployeeGroupOutputDto(
override fun Group.toOutput() = GroupOutputDto(
this.id!!,
this.name,
this.permissions,
@ -249,20 +252,20 @@ class EmployeeGroupServiceImpl(
)
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getEmployeesForGroup(id: Long): Collection<Employee> =
employeeService.getByGroup(getById(id))
override fun getUsersForGroup(id: Long): Collection<User> =
userService.getByGroup(getById(id))
@Transactional
override fun save(entity: EmployeeGroup): EmployeeGroup {
override fun save(entity: Group): Group {
return super<AbstractExternalNamedModelService>.save(entity).apply {
employeeService.saveDefaultGroupEmployee(this)
userService.saveDefaultGroupUser(this)
}
}
override fun update(entity: EmployeeGroupUpdateDto): EmployeeGroup {
override fun update(entity: GroupUpdateDto): Group {
val persistedGroup by lazy { getById(entity.id) }
return update(with(entity) {
EmployeeGroup(
Group(
entity.id,
if (name.isNotBlank()) entity.name else persistedGroup.name,
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
@ -271,15 +274,15 @@ class EmployeeGroupServiceImpl(
}
@Transactional
override fun delete(entity: EmployeeGroup) {
employeeService.delete(employeeService.getDefaultGroupEmployee(entity))
override fun delete(entity: Group) {
userService.delete(userService.getDefaultGroupUser(entity))
super.delete(entity)
}
override fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup {
override fun getRequestDefaultGroup(request: HttpServletRequest): Group {
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
?: throw NoDefaultGroupException()
val defaultGroupUser = employeeService.getById(
val defaultGroupUser = userService.getById(
defaultGroupCookie.value.toLong(),
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
@ -289,7 +292,7 @@ class EmployeeGroupServiceImpl(
override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) {
val group = getById(groupId)
val defaultGroupUser = employeeService.getDefaultGroupEmployee(group)
val defaultGroupUser = userService.getDefaultGroupUser(group)
response.addHeader(
"Set-Cookie",
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict"
@ -298,13 +301,14 @@ class EmployeeGroupServiceImpl(
}
@Service
class EmployeeUserDetailsServiceImpl(
private val employeeService: EmployeeService
@Profile("!emergency")
class CreUserDetailsServiceImpl(
private val userService: UserService
) :
EmployeeUserDetailsService {
CreUserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
try {
return loadUserByEmployeeId(username.toLong(), true)
return loadUserById(username.toLong(), true)
} catch (ex: NotFoundException) {
throw UsernameNotFoundException(username)
} catch (ex: NotFoundException) {
@ -312,12 +316,12 @@ class EmployeeUserDetailsServiceImpl(
}
}
override fun loadUserByEmployeeId(employeeId: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
val employee = employeeService.getById(
employeeId,
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
val user = userService.getById(
id,
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
ignoreSystemUsers = false
)
return User(employee.id.toString(), employee.password, employee.authorities)
return SpringUser(user.id.toString(), user.password, user.authorities)
}
}

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
@ -136,7 +138,7 @@ class MaterialServiceImpl(
override fun delete(entity: Material) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialException(entity)
fileService.delete(entity.simdutFilePath)
if (fileService.exists(entity.simdutFilePath)) fileService.delete(entity.simdutFilePath)
super.delete(entity)
}
}

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

@ -1,11 +1,13 @@
package dev.fyloz.colorrecipesexplorer.service
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.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
@ -16,6 +18,12 @@ interface RecipeService :
/** Checks if one or more recipes have the given [company]. */
fun existsByCompany(company: Company): Boolean
/** Checks if a recipe exists with the given [name] and [company]. */
fun existsByNameAndCompany(name: String, company: Company): Boolean
/** Gets all recipes with the given [name]. */
fun getAllByName(name: String): Collection<Recipe>
/** Gets all recipes with the given [company]. */
fun getAllByCompany(company: Company): Collection<Recipe>
@ -30,13 +38,15 @@ interface RecipeService :
}
@Service
@Profile("!emergency")
class RecipeServiceImpl(
recipeRepository: RecipeRepository,
val companyService: CompanyService,
val mixService: MixService,
val recipeStepService: RecipeStepService,
@Lazy val groupService: EmployeeGroupService,
val recipeImageService: RecipeImageService
@Lazy val groupService: GroupService,
val recipeImageService: RecipeImageService,
val configService: ConfigurationService
) :
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeOutputDto, RecipeRepository>(
recipeRepository
@ -62,15 +72,24 @@ class RecipeServiceImpl(
}.toSet(),
this.groupsInformation,
recipeImageService.getAllImages(this)
.map { this.imageUrl(it) }
.map { this.imageUrl(configService.get(ConfigurationType.INSTANCE_URL).content, it) }
.toSet()
)
override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company)
override fun getAllByCompany(company: Company): Collection<Recipe> = repository.findAllByCompany(company)
override fun existsByNameAndCompany(name: String, company: Company) =
repository.existsByNameAndCompany(name, company)
override fun getAllByName(name: String) = repository.findAllByName(name)
override fun getAllByCompany(company: Company) = repository.findAllByCompany(company)
override fun save(entity: RecipeSaveDto): Recipe {
// TODO checks if name is unique in the scope of the [company]
val company = companyService.getById(entity.companyId)
if (existsByNameAndCompany(entity.name, company)) {
throw recipeNameAlreadyExistsForCompanyException(entity.name, company)
}
return save(with(entity) {
recipe(
name = name,
@ -80,14 +99,23 @@ class RecipeServiceImpl(
sample = sample,
approbationDate = approbationDate,
remark = remark ?: "",
company = companyService.getById(companyId)
company = company
)
})
}
@Transactional
override fun update(entity: RecipeUpdateDto): Recipe {
val persistedRecipe by lazy { getById(entity.id) }
val persistedRecipe = getById(entity.id)
val name = entity.name
val company = persistedRecipe.company
if (name != null &&
name != persistedRecipe.name &&
existsByNameAndCompany(name, company)
) {
throw recipeNameAlreadyExistsForCompanyException(name, company)
}
return update(with(entity) {
recipe(
@ -99,7 +127,7 @@ class RecipeServiceImpl(
sample = sample ?: persistedRecipe.sample,
approbationDate = approbationDate ?: persistedRecipe.approbationDate,
remark = remark or persistedRecipe.remark,
company = persistedRecipe.company,
company = company,
mixes = persistedRecipe.mixes,
groupsInformation = updateGroupsInformation(persistedRecipe, entity)
)
@ -137,7 +165,7 @@ class RecipeServiceImpl(
if (publicDataDto.notes != null) {
val recipe = getById(publicDataDto.recipeId)
fun noteForGroup(group: EmployeeGroup) =
fun noteForGroup(group: Group) =
publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content
// Notes
@ -181,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

@ -2,9 +2,11 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.*
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
@ -21,6 +23,7 @@ interface RecipeStepService : ModelService<RecipeStep, RecipeStepRepository> {
}
@Service
@Profile("!emergency")
class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) :
AbstractModelService<RecipeStep, RecipeStepRepository>(recipeStepRepository),
RecipeStepService {
@ -81,8 +84,8 @@ class InvalidStepsPositionsException(
)
class InvalidGroupStepsPositionsException(
val group: EmployeeGroup,
val exception: InvalidStepsPositionsException
val group: Group,
val exception: InvalidStepsPositionsException
) : RestException(
"invalid-groupinformation-recipestep-position",
"Invalid steps positions",

View File

@ -1,78 +0,0 @@
package dev.fyloz.colorrecipesexplorer.service.files
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.utils.*
import org.springframework.core.io.ByteArrayResource
import org.springframework.stereotype.Service
private const val TOUCH_UP_KIT_FILES_PATH = "pdf/touchupkits"
const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE"
const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT"
interface TouchUpKitService {
/** Generates and returns a [PdfDocument] for the given [job]. */
fun generateJobPdf(job: String): PdfDocument
/**
* Generates and returns a [PdfDocument] for the given [job] as a [ByteArrayResource].
*
* If [CreProperties.cacheGeneratedFiles] is enabled and a file exists for the job, its content will be returned.
* If caching is enabled but no file exists for the job, the generated ByteArrayResource will be cached on the disk.
*/
fun generateJobPdfResource(job: String): ByteArrayResource
/** Writes the given [document] to the [FileService] if [CreProperties.cacheGeneratedFiles] is enabled. */
fun String.cachePdfDocument(document: PdfDocument)
}
@Service
class TouchUpKitServiceImpl(
private val fileService: FileService,
private val creProperties: CreProperties
) : TouchUpKitService {
override fun generateJobPdf(job: String) = pdf {
container {
centeredVertically = true
drawContainerBottom = true
text(TOUCH_UP_TEXT_FR) {
bold = true
fontSize = PDF_DEFAULT_FONT_SIZE + 12
}
text(TOUCH_UP_TEXT_EN) {
bold = true
fontSize = PDF_DEFAULT_FONT_SIZE + 12
}
text(job) {
marginTop = 10f
}
}
container(containers[0]) {
drawContainerBottom = false
}
}
override fun generateJobPdfResource(job: String): ByteArrayResource {
if (creProperties.cacheGeneratedFiles) {
with(job.pdfDocumentPath()) {
if (fileService.exists(this)) {
return fileService.read(this)
}
}
}
return generateJobPdf(job).apply {
job.cachePdfDocument(this)
}.toByteArrayResource()
}
override fun String.cachePdfDocument(document: PdfDocument) {
if (!creProperties.cacheGeneratedFiles) return
fileService.write(document.toByteArrayResource(), this.pdfDocumentPath(), true)
}
private fun String.pdfDocumentPath() =
"$TOUCH_UP_KIT_FILES_PATH/$this.pdf"
}

View File

@ -0,0 +1,132 @@
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
private const val TOUCH_UP_KIT_FILES_PATH = "pdf/touchupkits"
const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE"
const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT"
interface TouchUpKitService :
ExternalModelService<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository> {
/** Generates and returns a [PdfDocument] for the given [job]. */
fun generateJobPdf(job: String): PdfDocument
/**
* Generates and returns a [PdfDocument] for the given [job] as a [ByteArrayResource].
*
* If [CreProperties.cacheGeneratedFiles] is enabled and a file exists for the job, its content will be returned.
* If caching is enabled but no file exists for the job, the generated ByteArrayResource will be cached on the disk.
*/
fun generateJobPdfResource(job: String): ByteArrayResource
/** Writes the given [document] to the [FileService] if [CreProperties.cacheGeneratedFiles] is enabled. */
fun String.cachePdfDocument(document: PdfDocument)
}
@Service
@Profile("!emergency")
class TouchUpKitServiceImpl(
private val fileService: FileService,
private val configService: ConfigurationService,
touchUpKitRepository: TouchUpKitRepository
) : AbstractExternalModelService<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository>(
touchUpKitRepository
), TouchUpKitService {
override fun idNotFoundException(id: Long) = touchUpKitIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = touchUpKitIdAlreadyExistsException(id)
override fun TouchUpKit.toOutput() = TouchUpKitOutputDto(
this.id!!,
this.project,
this.buggy,
this.company,
this.quantity,
this.shippingDate,
this.finish,
this.material,
this.content,
this.pdfUrl()
)
override fun update(entity: TouchUpKitUpdateDto): TouchUpKit {
val persistedKit by lazy { getById(entity.id) }
return super.update(with(entity) {
touchUpKit(
id = id,
project = project ?: persistedKit.project,
buggy = buggy ?: persistedKit.buggy,
company = company ?: persistedKit.company,
quantity = quantity ?: persistedKit.quantity,
shippingDate = shippingDate ?: persistedKit.shippingDate,
finish = finish ?: persistedKit.finish,
material = material ?: persistedKit.material,
content = content?.map { touchUpKitProduct(it) }?.toSet() ?: persistedKit.content
)
})
}
override fun generateJobPdf(job: String) = pdf {
container {
centeredVertically = true
drawContainerBottom = true
text(TOUCH_UP_TEXT_FR) {
bold = true
fontSize = PDF_DEFAULT_FONT_SIZE + 12
}
text(TOUCH_UP_TEXT_EN) {
bold = true
fontSize = PDF_DEFAULT_FONT_SIZE + 12
}
text(job) {
marginTop = 10f
}
}
container(containers[0]) {
drawContainerBottom = false
}
}
override fun generateJobPdfResource(job: String): ByteArrayResource {
if (cacheGeneratedFiles()) {
with(job.pdfDocumentPath()) {
if (fileService.exists(this)) {
return fileService.read(this)
}
}
}
return generateJobPdf(job).apply {
job.cachePdfDocument(this)
}.toByteArrayResource()
}
override fun String.cachePdfDocument(document: PdfDocument) {
if (!cacheGeneratedFiles()) return
fileService.write(document.toByteArrayResource(), this.pdfDocumentPath(), true)
}
private fun String.pdfDocumentPath() =
"$TOUCH_UP_KIT_FILES_PATH/$this.pdf"
private fun TouchUpKit.pdfUrl() =
"${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,11 +17,8 @@ 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=true
spring.jpa.show-sql=false
# Do not modify
spring.messages.fallback-to-system-locale=true
spring.servlet.multipart.max-file-size=10MB
@ -30,4 +27,8 @@ spring.jpa.open-in-view=true
server.http2.enabled=true
server.error.whitelabel.enabled=false
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

@ -1 +0,0 @@
junit.jupiter.testinstance.lifecycle.default=per_class

View File

@ -4,15 +4,11 @@ import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.EmployeeGroupRepository
import dev.fyloz.colorrecipesexplorer.repository.EmployeeRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import org.junit.jupiter.api.*
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import java.util.*
@ -22,24 +18,26 @@ import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import org.springframework.security.core.userdetails.User as SpringUser
class EmployeeServiceTest :
AbstractExternalModelServiceTest<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeService, EmployeeRepository>() {
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserServiceTest :
AbstractExternalModelServiceTest<User, UserSaveDto, UserUpdateDto, UserService, UserRepository>() {
private val passwordEncoder = BCryptPasswordEncoder()
override val entity: Employee = employee(passwordEncoder, id = 0L)
override val anotherEntity: Employee = employee(passwordEncoder, id = 1L)
private val entityDefaultGroupUser = employee(passwordEncoder, id = 2L, isDefaultGroupUser = true)
private val entitySystemUser = employee(passwordEncoder, id = 3L, isSystemUser = true)
private val group = employeeGroup(id = 0L)
override val entitySaveDto: EmployeeSaveDto = spy(employeeSaveDto(passwordEncoder, id = 0L))
override val entityUpdateDto: EmployeeUpdateDto = spy(employeeUpdateDto(id = 0L))
override val entity: User = user(passwordEncoder, id = 0L)
override val anotherEntity: User = user(passwordEncoder, id = 1L)
private val entityDefaultGroupUser = user(passwordEncoder, id = 2L, isDefaultGroupUser = true)
private val entitySystemUser = user(passwordEncoder, id = 3L, isSystemUser = true)
private val group = group(id = 0L)
override val entitySaveDto: UserSaveDto = spy(userSaveDto(passwordEncoder, id = 0L))
override val entityUpdateDto: UserUpdateDto = spy(userUpdateDto(id = 0L))
override val repository: EmployeeRepository = mock()
private val employeeGroupService: EmployeeGroupService = mock()
override val service: EmployeeService = spy(EmployeeServiceImpl(repository, employeeGroupService, passwordEncoder))
override val repository: UserRepository = mock()
private val groupService: GroupService = mock()
override val service: UserService = spy(UserServiceImpl(repository, groupService, passwordEncoder))
private val entitySaveDtoEmployee = Employee(
private val entitySaveDtoUser = User(
entitySaveDto.id,
entitySaveDto.firstName,
entitySaveDto.lastName,
@ -52,14 +50,14 @@ class EmployeeServiceTest :
@AfterEach
override fun afterEach() {
reset(employeeGroupService)
reset(groupService)
super.afterEach()
}
// existsByFirstNameAndLastName()
@Test
fun `existsByFirstNameAndLastName() returns true when an employee with the given first name and last name exists`() {
fun `existsByFirstNameAndLastName() returns true when an user with the given first name and last name exists`() {
whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(true)
val found = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName)
@ -68,7 +66,7 @@ class EmployeeServiceTest :
}
@Test
fun `existsByFirstNameAndLastName() returns false when no employee with the given first name and last name exists`() {
fun `existsByFirstNameAndLastName() returns false when no user with the given first name and last name exists`() {
whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(false)
val found = service.existsByFirstNameAndLastName(entity.firstName, entity.lastName)
@ -79,7 +77,7 @@ class EmployeeServiceTest :
// getById()
@Test
fun `getById() throws NotFoundException when the corresponding employee is a default group user`() {
fun `getById() throws NotFoundException when the corresponding user is a default group user`() {
whenever(repository.findById(entityDefaultGroupUser.id)).doReturn(Optional.of(entityDefaultGroupUser))
assertThrows<NotFoundException> {
@ -92,7 +90,7 @@ class EmployeeServiceTest :
}
@Test
fun `getById() throws NotFoundException when the corresponding employee is a system user`() {
fun `getById() throws NotFoundException when the corresponding user is a system user`() {
whenever(repository.findById(entitySystemUser.id)).doReturn(Optional.of(entitySystemUser))
assertThrows<NotFoundException> {
@ -107,7 +105,7 @@ class EmployeeServiceTest :
// getByGroup()
@Test
fun `getByGroup() returns all the employees with the given group from the repository`() {
fun `getByGroup() returns all the users with the given group from the repository`() {
whenever(repository.findAllByGroup(group)).doReturn(entityList)
val found = service.getByGroup(group)
@ -117,7 +115,7 @@ class EmployeeServiceTest :
}
@Test
fun `getByGroup() returns an empty list when there is no employee with the given group in the repository`() {
fun `getByGroup() returns an empty list when there is no user with the given group in the repository`() {
whenever(repository.findAllByGroup(group)).doReturn(listOf())
val found = service.getByGroup(group)
@ -128,10 +126,10 @@ class EmployeeServiceTest :
// getDefaultGroupUser()
@Test
fun `getDefaultGroupUser() returns the default employee of the given group from the repository`() {
fun `getDefaultGroupUser() returns the default user of the given group from the repository`() {
whenever(repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)).doReturn(entityDefaultGroupUser)
val found = service.getDefaultGroupEmployee(group)
val found = service.getDefaultGroupUser(group)
assertEquals(entityDefaultGroupUser, found)
}
@ -166,13 +164,13 @@ class EmployeeServiceTest :
}
@Test
fun `save(dto) calls and returns save() with the created employee`() {
doReturn(entitySaveDtoEmployee).whenever(service).save(any<Employee>())
fun `save(dto) calls and returns save() with the created user`() {
doReturn(entitySaveDtoUser).whenever(service).save(any<User>())
val found = service.save(entitySaveDto)
verify(service).save(argThat<Employee> { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName })
assertEquals(entitySaveDtoEmployee, found)
verify(service).save(argThat<User> { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName })
assertEquals(entitySaveDtoUser, found)
}
// update()
@ -182,7 +180,7 @@ class EmployeeServiceTest :
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
@Test
fun `update() throws AlreadyExistsException when a different employee with the given first name and last name exists`() {
fun `update() throws AlreadyExistsException when a different user with the given first name and last name exists`() {
whenever(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(
entityDefaultGroupUser
)
@ -198,47 +196,48 @@ class EmployeeServiceTest :
}
}
class EmployeeGroupServiceTest :
AbstractExternalNamedModelServiceTest<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupService, EmployeeGroupRepository>() {
private val employeeService: EmployeeService = mock()
override val repository: EmployeeGroupRepository = mock()
override val service: EmployeeGroupServiceImpl = spy(EmployeeGroupServiceImpl(employeeService, repository))
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GroupServiceTest :
AbstractExternalNamedModelServiceTest<Group, GroupSaveDto, GroupUpdateDto, GroupService, GroupRepository>() {
private val userService: UserService = mock()
override val repository: GroupRepository = mock()
override val service: GroupServiceImpl = spy(GroupServiceImpl(userService, repository))
override val entity: EmployeeGroup = employeeGroup(id = 0L, name = "group")
override val anotherEntity: EmployeeGroup = employeeGroup(id = 1L, name = "another group")
override val entitySaveDto: EmployeeGroupSaveDto = spy(employeeGroupSaveDto(name = "group"))
override val entityUpdateDto: EmployeeGroupUpdateDto = spy(employeeGroupUpdateDto(id = 0L, name = "group"))
override val entityWithEntityName: EmployeeGroup = employeeGroup(id = 2L, name = entity.name)
override val entity: Group = group(id = 0L, name = "group")
override val anotherEntity: Group = group(id = 1L, name = "another group")
override val entitySaveDto: GroupSaveDto = spy(groupSaveDto(name = "group"))
override val entityUpdateDto: GroupUpdateDto = spy(groupUpdateDto(id = 0L, name = "group"))
override val entityWithEntityName: Group = group(id = 2L, name = entity.name)
private val groupEmployeeId = 1000000L + entity.id!!
private val groupEmployee = employee(BCryptPasswordEncoder(), id = groupEmployeeId, group = entity)
private val groupUserId = 1000000L + entity.id!!
private val groupUser = user(BCryptPasswordEncoder(), id = groupUserId, group = entity)
@BeforeEach
override fun afterEach() {
reset(employeeService)
reset(userService)
super.afterEach()
}
// getEmployeesForGroup()
// getUsersForGroup()
@Test
fun `getEmployeesForGroup() returns all employees in the given group`() {
val group = employeeGroup(id = 1L)
fun `getUsersForGroup() returns all users in the given group`() {
val group = group(id = 1L)
doReturn(group).whenever(service).getById(group.id!!)
whenever(employeeService.getByGroup(group)).doReturn(listOf(groupEmployee))
whenever(userService.getByGroup(group)).doReturn(listOf(groupUser))
val found = service.getEmployeesForGroup(group.id!!)
val found = service.getUsersForGroup(group.id!!)
assertTrue(found.contains(groupEmployee))
assertTrue(found.contains(groupUser))
assertTrue(found.size == 1)
}
@Test
fun `getEmployeesForGroup() returns empty collection when the given group contains any employee`() {
fun `getUsersForGroup() returns empty collection when the given group contains any user`() {
doReturn(entity).whenever(service).getById(entity.id!!)
val found = service.getEmployeesForGroup(entity.id!!)
val found = service.getUsersForGroup(entity.id!!)
assertTrue(found.isEmpty())
}
@ -247,11 +246,11 @@ class EmployeeGroupServiceTest :
@Test
fun `getRequestDefaultGroup() returns the group contained in the cookie of the HTTP request`() {
val cookies: Array<Cookie> = arrayOf(Cookie(defaultGroupCookieName, groupEmployeeId.toString()))
val cookies: Array<Cookie> = arrayOf(Cookie(defaultGroupCookieName, groupUserId.toString()))
val request: HttpServletRequest = mock()
whenever(request.cookies).doReturn(cookies)
whenever(employeeService.getById(eq(groupEmployeeId), any(), any())).doReturn(groupEmployee)
whenever(userService.getById(eq(groupUserId), any(), any())).doReturn(groupUser)
val found = service.getRequestDefaultGroup(request)
@ -273,7 +272,7 @@ class EmployeeGroupServiceTest :
fun `setResponseDefaultGroup() the default group cookie has been added to the given HTTP response with the given group id`() {
val response = MockHttpServletResponse()
whenever(employeeService.getDefaultGroupEmployee(entity)).doReturn(groupEmployee)
whenever(userService.getDefaultGroupUser(entity)).doReturn(groupUser)
doReturn(entity).whenever(service).getById(entity.id!!)
service.setResponseDefaultGroup(entity.id!!, response)
@ -281,7 +280,7 @@ class EmployeeGroupServiceTest :
assertNotNull(found)
assertEquals(defaultGroupCookieName, found.name)
assertEquals(groupEmployeeId.toString(), found.value)
assertEquals(groupUserId.toString(), found.value)
assertEquals(defaultGroupCookieMaxAge, found.maxAge)
assertTrue(found.isHttpOnly)
assertTrue(found.secure)
@ -301,48 +300,49 @@ class EmployeeGroupServiceTest :
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
}
class EmployeeUserDetailsServiceTest {
private val employeeService: EmployeeService = mock()
private val service = spy(EmployeeUserDetailsServiceImpl(employeeService))
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserUserDetailsServiceTest {
private val userService: UserService = mock()
private val service = spy(CreUserDetailsServiceImpl(userService))
private val employee = employee(id = 0L)
private val user = user(id = 0L)
@BeforeEach
fun beforeEach() {
reset(employeeService, service)
reset(userService, service)
}
// loadUserByUsername()
@Test
fun `loadUserByUsername() calls loadUserByEmployeeId() with the given username as an id`() {
whenever(employeeService.getById(eq(employee.id), any(), any())).doReturn(employee)
doReturn(User(employee.id.toString(), employee.password, listOf())).whenever(service)
.loadUserByEmployeeId(employee.id)
fun `loadUserByUsername() calls loadUserByUserId() with the given username as an id`() {
whenever(userService.getById(eq(user.id), any(), any())).doReturn(user)
doReturn(SpringUser(user.id.toString(), user.password, listOf())).whenever(service)
.loadUserById(user.id)
service.loadUserByUsername(employee.id.toString())
service.loadUserByUsername(user.id.toString())
verify(service).loadUserByEmployeeId(eq(employee.id), any())
verify(service).loadUserById(eq(user.id), any())
}
@Test
fun `loadUserByUsername() throws UsernameNotFoundException when no employee with the given id exists`() {
whenever(employeeService.getById(eq(employee.id), any(), any())).doThrow(
employeeIdNotFoundException(employee.id)
fun `loadUserByUsername() throws UsernameNotFoundException when no user with the given id exists`() {
whenever(userService.getById(eq(user.id), any(), any())).doThrow(
userIdNotFoundException(user.id)
)
assertThrows<UsernameNotFoundException> { service.loadUserByUsername(employee.id.toString()) }
assertThrows<UsernameNotFoundException> { service.loadUserByUsername(user.id.toString()) }
}
// loadUserByEmployeeId
// loadUserByUserId
@Test
fun `loadUserByEmployeeId() returns an User corresponding to the employee with the given id`() {
whenever(employeeService.getById(eq(employee.id), any(), any())).doReturn(employee)
fun `loadUserByUserId() returns an User corresponding to the user with the given id`() {
whenever(userService.getById(eq(user.id), any(), any())).doReturn(user)
val found = service.loadUserByEmployeeId(employee.id)
val found = service.loadUserById(user.id)
assertEquals(employee.id, found.username.toLong())
assertEquals(employee.password, found.password)
assertEquals(user.id, found.username.toLong())
assertEquals(user.password, found.password)
}
}

View File

@ -5,9 +5,11 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CompanyServiceTest :
AbstractExternalNamedModelServiceTest<Company, CompanySaveDto, CompanyUpdateDto, CompanyService, CompanyRepository>() {
private val recipeService: RecipeService = mock()

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

@ -4,10 +4,12 @@ import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.model.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class InventoryServiceTest {
private val materialService: MaterialService = mock()
private val mixService: MixService = mock()

View File

@ -7,12 +7,14 @@ import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository
import dev.fyloz.colorrecipesexplorer.service.files.FileService
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import org.springframework.mock.web.MockMultipartFile
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MaterialServiceTest :
AbstractExternalNamedModelServiceTest<Material, MaterialSaveDto, MaterialUpdateDto, MaterialService, MaterialRepository>() {
override val repository: MaterialRepository = mock()
@ -21,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

@ -7,11 +7,13 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MaterialTypeServiceTest :
AbstractExternalNamedModelServiceTest<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeService, MaterialTypeRepository>() {
override val repository: MaterialTypeRepository = mock()

View File

@ -4,12 +4,14 @@ import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MixMaterialServiceTest : AbstractModelServiceTest<MixMaterial, MixMaterialService, MixMaterialRepository>() {
override val repository: MixMaterialRepository = mock()
private val materialService: MaterialService = mock()

View File

@ -5,9 +5,11 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpdateDto, MixService, MixRepository>() {
override val repository: MixRepository = mock()
private val recipeService: RecipeService = mock()

View File

@ -7,10 +7,12 @@ import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MixTypeServiceTest : AbstractNamedModelServiceTest<MixType, MixTypeService, MixTypeRepository>() {
override val repository: MixTypeRepository = mock()
private val materialService: MaterialService = mock()

View File

@ -1,12 +1,16 @@
package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.account.group
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.colorrecipesexplorer.service.files.FileService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import org.springframework.mock.web.MockMultipartFile
import org.springframework.web.multipart.MultipartFile
import java.io.File
@ -14,15 +18,16 @@ import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RecipeServiceTest :
AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() {
override val repository: RecipeRepository = mock()
private val companyService: CompanyService = mock()
private val mixService: MixService = mock()
private val groupService: EmployeeGroupService = mock()
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)
@ -56,6 +61,32 @@ class RecipeServiceTest :
assertFalse(found)
}
// existsByNameAndCompany()
@Test
fun `existsByNameAndCompany() returns if a recipe exists for the given name and company in the repository`() {
setOf(true, false).forEach {
whenever(repository.existsByNameAndCompany(entity.name, company)).doReturn(it)
val exists = service.existsByNameAndCompany(entity.name, company)
assertEquals(it, exists)
}
}
// getAllByName()
@Test
fun `getAllByName() returns the recipes with the given name`() {
val recipes = listOf(entity, anotherEntity)
whenever(repository.findAllByName(entity.name)).doReturn(recipes)
val found = service.getAllByName(entity.name)
assertEquals(recipes, found)
}
// getAllByCompany()
@Test
@ -73,14 +104,40 @@ class RecipeServiceTest :
@Test
override fun `save(dto) calls and returns save() with the created entity`() {
whenever(companyService.getById(company.id!!)).doReturn(company)
doReturn(false).whenever(service).existsByNameAndCompany(entity.name, company)
withBaseSaveDtoTest(entity, entitySaveDto, service, { argThat { this.id == null && this.color == color } })
}
@Test
fun `save(dto) throw AlreadyExistsException when a recipe with the given name and company exists in the repository`() {
whenever(companyService.getById(company.id!!)).doReturn(company)
doReturn(true).whenever(service).existsByNameAndCompany(entity.name, company)
with(assertThrows<AlreadyExistsException> { service.save(entitySaveDto) }) {
this.assertErrorCode("company-name")
}
}
// update()
@Test
override fun `update(dto) calls and returns update() with the created entity`() =
override fun `update(dto) calls and returns update() with the created entity`() {
doReturn(false).whenever(service).existsByNameAndCompany(entity.name, company)
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
}
@Test
fun `update(dto) throws AlreadyExistsException when a recipe exists for the given name and company`() {
val name = "another recipe"
doReturn(entity).whenever(service).getById(entity.id!!)
doReturn(true).whenever(service).existsByNameAndCompany(name, company)
doReturn(name).whenever(entityUpdateDto).name
with(assertThrows<AlreadyExistsException> { service.update(entityUpdateDto) }) {
this.assertErrorCode("company-name")
}
}
// updatePublicData()
@ -88,9 +145,9 @@ class RecipeServiceTest :
fun `updatePublicData() updates the notes of a recipe groups information according to the RecipePublicDataDto`() {
val recipe = recipe(
id = 0L, groupsInformation = setOf(
recipeGroupInformation(id = 0L, group = employeeGroup(id = 1L), note = "Old note"),
recipeGroupInformation(id = 1L, group = employeeGroup(id = 2L), note = "Another note"),
recipeGroupInformation(id = 2L, group = employeeGroup(id = 3L), note = "Up to date note")
recipeGroupInformation(id = 0L, group = group(id = 1L), note = "Old note"),
recipeGroupInformation(id = 1L, group = group(id = 2L), note = "Another note"),
recipeGroupInformation(id = 2L, group = group(id = 3L), note = "Up to date note")
)
)
val notes = setOf(

View File

@ -2,11 +2,14 @@ package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.account.group
import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RecipeStepServiceTest :
AbstractModelServiceTest<RecipeStep, RecipeStepService, RecipeStepRepository>() {
override val repository: RecipeStepRepository = mock()
@ -80,7 +83,7 @@ class RecipeStepServiceTest :
private fun withGroupInformation(steps: MutableSet<RecipeStep>? = null, test: RecipeGroupInformation.() -> Unit) {
recipeGroupInformation(
group = employeeGroup(id = 0L),
group = group(id = 0L),
steps = steps ?: mutableSetOf(
recipeStep(id = 0L, position = 1),
recipeStep(id = 1L, position = 2),

View File

@ -1,6 +1,13 @@
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
import dev.fyloz.colorrecipesexplorer.utils.PdfDocument
import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource
import io.mockk.*
@ -10,13 +17,15 @@ import org.springframework.core.io.ByteArrayResource
import kotlin.test.assertEquals
private class TouchUpKitServiceTestContext {
val touchUpKitRepository = mockk<TouchUpKitRepository>()
val fileService = mockk<FileService> {
every { write(any<ByteArrayResource>(), any(), any()) } just Runs
}
val creProperties = mockk<CreProperties> {
every { cacheGeneratedFiles } returns false
}
val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, 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)
@ -76,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)
@ -104,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)