From a59bad7a7abe6f1f4dae9ad95909faf6bfe9c69b Mon Sep 17 00:00:00 2001 From: FyloZ Date: Tue, 4 May 2021 12:04:02 -0400 Subject: [PATCH 1/9] =?UTF-8?q?Ajout=20des=20conflits=20de=20nom=20entre?= =?UTF-8?q?=20les=20recettes=20d'une=20banni=C3=A8re=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/RestException.kt | 99 +++++++++---------- .../colorrecipesexplorer/model/Recipe.kt | 15 +++ .../repository/RecipeRepository.kt | 3 + .../service/RecipeService.kt | 28 +++++- .../service/RecipeServiceTest.kt | 43 +++++++- 5 files changed, 133 insertions(+), 55 deletions(-) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt index 2732f61..1bc62b7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt @@ -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 = mapOf() + val errorCode: String, + val title: String, + val status: HttpStatus, + val details: String, + val extensions: Map = 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 = 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 { val errors = hashMapOf() ex.bindingResult.allErrors.forEach { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index 367375a..75c0287 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -288,6 +288,8 @@ private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found" private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists" private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe" +sealed class RecipeException + fun recipeIdNotFoundException(id: Long) = NotFoundException( RECIPE_EXCEPTION_ERROR_CODE, @@ -303,3 +305,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!! + ) + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt index c3b9e4b..a7284f7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt @@ -8,6 +8,9 @@ interface RecipeRepository : JpaRepository { /** 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 [company]. */ fun findAllByCompany(company: Company): Collection } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index 4ace03b..b170e81 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -16,6 +16,9 @@ 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 [company]. */ fun getAllByCompany(company: Company): Collection @@ -67,10 +70,18 @@ class RecipeServiceImpl( ) override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company) + override fun existsByNameAndCompany(name: String, company: Company) = + repository.existsByNameAndCompany(name, company) + override fun getAllByCompany(company: Company): Collection = 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 +91,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 +119,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) ) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt index 98dd0f4..859f4d9 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt @@ -1,12 +1,14 @@ package dev.fyloz.colorrecipesexplorer.service import com.nhaarman.mockitokotlin2.* +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.model.* 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.assertThrows import org.springframework.mock.web.MockMultipartFile import org.springframework.web.multipart.MultipartFile import java.io.File @@ -56,6 +58,19 @@ 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) + } + } + // getAllByCompany() @Test @@ -73,14 +88,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 { 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 { service.update(entityUpdateDto) }) { + this.assertErrorCode("company-name") + } + } // updatePublicData() From 8f761a4be43c7f041ce38e433fad5858ee453a32 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Wed, 5 May 2021 23:49:12 -0400 Subject: [PATCH 2/9] Employee -> User --- .../config/WebSecurityConfig.kt | 70 +++--- .../colorrecipesexplorer/model/Employee.kt | 193 ---------------- .../model/EmployeeGroup.kt | 141 ------------ .../colorrecipesexplorer/model/Recipe.kt | 6 +- .../model/account/Group.kt | 143 ++++++++++++ .../Permission.kt} | 14 +- .../model/account/User.kt | 195 ++++++++++++++++ .../repository/AccountRepository.kt | 14 +- .../rest/AccountControllers.kt | 82 +++---- .../service/AccountService.kt | 216 +++++++++--------- .../service/RecipeService.kt | 5 +- .../service/RecipeStepService.kt | 5 +- .../service/AccountsServiceTest.kt | 154 ++++++------- .../service/RecipeServiceTest.kt | 9 +- .../service/RecipeStepServiceTest.kt | 3 +- 15 files changed, 630 insertions(+), 620 deletions(-) delete mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Employee.kt delete mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeeGroup.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/{EmployeePermission.kt => account/Permission.kt} (88%) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt index 0d4834c..1fb70ea 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt @@ -2,13 +2,13 @@ package dev.fyloz.colorrecipesexplorer.config import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 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.UserLoginRequest +import dev.fyloz.colorrecipesexplorer.model.account.Permission +import dev.fyloz.colorrecipesexplorer.model.account.User +import dev.fyloz.colorrecipesexplorer.service.UserService +import dev.fyloz.colorrecipesexplorer.service.UserServiceImpl +import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsService +import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsServiceImpl import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm @@ -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.User as SpringUser import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter @@ -52,11 +52,11 @@ import javax.servlet.http.HttpServletResponse @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: CreUserDetailsServiceImpl, + @Lazy val userService: UserServiceImpl, + val environment: Environment, + val logger: Logger ) : WebSecurityConfigurerAdapter() { var debugMode = false @@ -95,15 +95,15 @@ class WebSecurityConfig( credentials: SecurityConfigurationProperties.SystemUserCredentials?, firstName: String, lastName: String, - permissions: List + permissions: List ) { 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( + if (!userService.existsById(credentials.id!!)) { + userService.save( + User( id = credentials.id!!, firstName = firstName, lastName = lastName, @@ -115,7 +115,7 @@ class WebSecurityConfig( } } - 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!") } @@ -128,7 +128,7 @@ class WebSecurityConfig( .addFilter( JwtAuthenticationFilter( authenticationManager(), - employeeService, + userService, securityConfigurationProperties ) ) @@ -145,7 +145,7 @@ class WebSecurityConfig( http.authorizeRequests() .antMatchers("/api/login").permitAll() .antMatchers("/api/logout").authenticated() - .antMatchers("/api/employee/current").authenticated() + .antMatchers("/api/user/current").authenticated() .anyRequest().authenticated() } else { http @@ -171,9 +171,9 @@ const val defaultGroupCookieName = "Default-Group" val blacklistedJwtTokens = mutableListOf() class JwtAuthenticationFilter( - private val authManager: AuthenticationManager, - private val employeeService: EmployeeService, - private val securityConfigurationProperties: SecurityConfigurationProperties + private val authManager: AuthenticationManager, + private val userService: UserService, + private val securityConfigurationProperties: SecurityConfigurationProperties ) : UsernamePasswordAuthenticationFilter() { private var debugMode = false @@ -183,7 +183,7 @@ 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)) } @@ -197,12 +197,12 @@ class JwtAuthenticationFilter( 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 + userService.updateLastLoginTime(userId.toLong()) val expirationMs = System.currentTimeMillis() + jwtDuration!! val expirationDate = Date(expirationMs) val token = Jwts.builder() - .setSubject(employeeId) + .setSubject(userId) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray()) .compact() @@ -220,9 +220,9 @@ class JwtAuthenticationFilter( } class JwtAuthorizationFilter( - private val userDetailsService: EmployeeUserDetailsService, - private val securityConfigurationProperties: SecurityConfigurationProperties, - authenticationManager: AuthenticationManager + private val userDetailsService: CreUserDetailsService, + private val securityConfigurationProperties: SecurityConfigurationProperties, + authenticationManager: AuthenticationManager ) : BasicAuthenticationFilter(authenticationManager) { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { fun tryLoginFromBearer(): Boolean { @@ -259,20 +259,20 @@ class JwtAuthorizationFilter( val jwtSecret = securityConfigurationProperties.jwtSecret Assert.notNull(jwtSecret, "No JWT secret has been defined.") return try { - val employeeId = Jwts.parser() + val userId = Jwts.parser() .setSigningKey(jwtSecret!!.toByteArray()) .parseClaimsJws(token.replace("Bearer", "")) .body .subject - if (employeeId != null) getAuthenticationToken(employeeId) else null + 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 = userDetailsService.loadUserById(userId.toLong(), false) + UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities) } catch (_: NotFoundException) { null } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Employee.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Employee.kt deleted file mode 100644 index 1b98d41..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Employee.kt +++ /dev/null @@ -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 = mutableSetOf(), - - @Column(name = "last_login_time") - var lastLoginTime: LocalDateTime? = null -) : Model { - val flatPermissions: Set - get() = permissions - .flatMap { it.flat() } - .filter { !it.deprecated } - .toMutableSet() - .apply { - if (group != null) this.addAll(group!!.flatPermissions) - } - - val authorities: Set - 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 = mutableSetOf() -) : EntityDto - -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? -) : EntityDto - -data class EmployeeOutputDto( - override val id: Long, - val firstName: String, - val lastName: String, - val group: EmployeeGroup?, - val permissions: Set, - val explicitPermissions: Set, - 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 = 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 = 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 = 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" - ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeeGroup.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeeGroup.kt deleted file mode 100644 index a88e0e4..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeeGroup.kt +++ /dev/null @@ -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 = mutableSetOf(), -) : NamedModel { - val flatPermissions: Set - 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 -) : EntityDto { - 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 -) : EntityDto { - override fun toEntity(): EmployeeGroup = - EmployeeGroup(id, name, permissions) -} - -data class EmployeeGroupOutputDto( - override val id: Long, - val name: String, - val permissions: Set, - val explicitPermissions: Set -): Model - -fun employeeGroup( - id: Long? = null, - name: String = "name", - permissions: MutableSet = mutableSetOf(), - op: EmployeeGroup.() -> Unit = {} -) = EmployeeGroup(id, name, permissions).apply(op) - -fun employeeGroupSaveDto( - name: String = "name", - permissions: MutableSet = mutableSetOf(), - op: EmployeeGroupSaveDto.() -> Unit = {} -) = EmployeeGroupSaveDto(name, permissions).apply(op) - -fun employeeGroupUpdateDto( - id: Long = 0L, - name: String = "name", - permissions: MutableSet = 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" - ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index 75c0287..cf0aad3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -3,6 +3,8 @@ 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.account.Group +import dev.fyloz.colorrecipesexplorer.model.account.group import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES @@ -176,7 +178,7 @@ data class RecipeGroupInformation( @ManyToOne @JoinColumn(name = "group_id") - val group: EmployeeGroup, + val group: Group, var note: String?, @@ -264,7 +266,7 @@ fun recipeUpdateDto( fun recipeGroupInformation( id: Long? = null, - group: EmployeeGroup = employeeGroup(), + group: Group = group(), note: String? = null, steps: MutableSet? = mutableSetOf(), op: RecipeGroupInformation.() -> Unit = {} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt new file mode 100644 index 0000000..4bc229d --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt @@ -0,0 +1,143 @@ +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.EntityDto +import dev.fyloz.colorrecipesexplorer.model.Model +import dev.fyloz.colorrecipesexplorer.model.NamedModel +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 = "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 = mutableSetOf(), +) : NamedModel { + val flatPermissions: Set + get() = this.permissions + .flatMap { it.flat() } + .filter { !it.deprecated } + .toSet() +} + +open class GroupSaveDto( + @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 +) : EntityDto { + override fun toEntity(): Group = + Group(null, name, permissions) +} + +open class GroupUpdateDto( + @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 +) : EntityDto { + override fun toEntity(): Group = + Group(id, name, permissions) +} + +data class GroupOutputDto( + override val id: Long, + val name: String, + val permissions: Set, + val explicitPermissions: Set +): Model + +fun group( + id: Long? = null, + name: String = "name", + permissions: MutableSet = mutableSetOf(), + op: Group.() -> Unit = {} +) = Group(id, name, permissions).apply(op) + +fun groupSaveDto( + name: String = "name", + permissions: MutableSet = mutableSetOf(), + op: GroupSaveDto.() -> Unit = {} +) = GroupSaveDto(name, permissions).apply(op) + +fun groupUpdateDto( + id: Long = 0L, + name: String = "name", + permissions: MutableSet = 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" + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Permission.kt similarity index 88% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Permission.kt index 5124243..333f8fb 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Permission.kt @@ -1,10 +1,10 @@ -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 = listOf(), +enum class Permission( + val impliedPermissions: List = listOf(), val deprecated: Boolean = false ) { READ_FILE, @@ -80,12 +80,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 { +fun Permission.flat(): Iterable { return mutableSetOf(this).apply { impliedPermissions.forEach { addAll(it.flat()) @@ -93,7 +93,7 @@ fun EmployeePermission.flat(): Iterable { } } -/** 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) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt new file mode 100644 index 0000000..c6e2b58 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt @@ -0,0 +1,195 @@ +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 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 USER_ID_NULL_MESSAGE = "Un numéro d'utilisateur est requis" +private const val USER_LAST_NAME_EMPTY_MESSAGE = "Un nom est requis" +private const val USER_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis" +private const val USER_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis" +private const val USER_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères" + +@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 = mutableSetOf(), + + @Column(name = "last_login_time") + var lastLoginTime: LocalDateTime? = null +) : Model { + val flatPermissions: Set + get() = permissions + .flatMap { it.flat() } + .filter { !it.deprecated } + .toMutableSet() + .apply { + if (group != null) this.addAll(group!!.flatPermissions) + } + + val authorities: Set + get() = flatPermissions.map { it.toAuthority() }.toMutableSet() +} + +/** DTO for creating users. Allows a [password] a [groupId]. */ +open class UserSaveDto( + @field:NotNull(message = USER_ID_NULL_MESSAGE) + val id: Long, + + @field:NotBlank(message = USER_FIRST_NAME_EMPTY_MESSAGE) + val firstName: String, + + @field:NotBlank(message = USER_LAST_NAME_EMPTY_MESSAGE) + val lastName: String, + + @field:NotBlank(message = USER_PASSWORD_EMPTY_MESSAGE) + @field:Size(min = 8, message = USER_PASSWORD_TOO_SHORT_MESSAGE) + val password: String, + + val groupId: Long?, + + @Enumerated(EnumType.STRING) + val permissions: MutableSet = mutableSetOf() +) : EntityDto + +open class UserUpdateDto( + @field:NotNull(message = USER_ID_NULL_MESSAGE) + val id: Long, + + @field:NullOrNotBlank(message = USER_FIRST_NAME_EMPTY_MESSAGE) + val firstName: String?, + + @field:NullOrNotBlank(message = USER_LAST_NAME_EMPTY_MESSAGE) + val lastName: String?, + + val groupId: Long?, + + @Enumerated(EnumType.STRING) + val permissions: Set? +) : EntityDto + +data class UserOutputDto( + override val id: Long, + val firstName: String, + val lastName: String, + val group: Group?, + val permissions: Set, + val explicitPermissions: Set, + 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 = 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 = 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 = 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" + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt index b1f6099..d2548ea 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/AccountRepository.kt @@ -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 { +interface UserRepository : JpaRepository { 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 + fun findAllByGroup(group: Group): Collection - fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: EmployeeGroup): Employee + fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: Group): User } @Repository -interface EmployeeGroupRepository : NamedJpaRepository +interface GroupRepository : NamedJpaRepository diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt index ea9efde..62f6d03 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt @@ -3,9 +3,9 @@ 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.http.MediaType import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* @@ -14,29 +14,29 @@ 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) +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 +47,56 @@ class EmployeeController(private val employeeService: EmployeeService) { @PostMapping @PreAuthorizeEditUsers - fun save(@Valid @RequestBody employee: EmployeeSaveDto) = - created(EMPLOYEE_CONTROLLER_PATH) { - with(employeeService) { - save(employee).toOutput() + fun save(@Valid @RequestBody user: UserSaveDto) = + created(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 fun deleteById(@PathVariable id: Long) = - employeeService.deleteById(id) + userService.deleteById(id) } @RestController -@RequestMapping(EMPLOYEE_GROUP_CONTROLLER_PATH) +@RequestMapping(GROUP_CONTROLLER_PATH) 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 +108,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 +132,8 @@ class GroupsController( @PostMapping @PreAuthorizeEditUsers - fun save(@Valid @RequestBody group: EmployeeGroupSaveDto) = - created(EMPLOYEE_GROUP_CONTROLLER_PATH) { + fun save(@Valid @RequestBody group: GroupSaveDto) = + created(GROUP_CONTROLLER_PATH) { with(groupService) { save(group).toOutput() } @@ -141,7 +141,7 @@ class GroupsController( @PutMapping @PreAuthorizeEditUsers - fun update(@Valid @RequestBody group: EmployeeGroupUpdateDto) = + fun update(@Valid @RequestBody group: GroupUpdateDto) = noContent { groupService.update(group) } @@ -156,10 +156,10 @@ class GroupsController( @RestController @RequestMapping("api") -class LogoutController(private val employeeService: EmployeeService) { +class LogoutController(private val userService: UserService) { @GetMapping("logout") fun logout(request: HttpServletRequest) = ok { - employeeService.logout(request) + userService.logout(request) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt index 8f08ed4..5d6bd11 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt @@ -3,12 +3,11 @@ 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.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException @@ -19,73 +18,74 @@ 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 { - /** Check if an [Employee] with the given [firstName] and [lastName] exists. */ +interface UserService : + ExternalModelService { + /** 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 + /** Gets all users which have the given [group]. */ + fun getByGroup(group: Group): Collection /** 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 { - /** Gets all the employees of the group with the given [id]. */ - fun getEmployeesForGroup(id: Long): Collection +interface GroupService : + ExternalNamedModelService { + /** Gets all the users of the group with the given [id]. */ + fun getUsersForGroup(id: Long): Collection /** 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, +class UserServiceImpl( + userRepository: UserRepository, + @Lazy val groupService: GroupService, @Lazy val passwordEncoder: PasswordEncoder, -) : AbstractExternalModelService( - employeeRepository +) : AbstractExternalModelService( + 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 +98,29 @@ class EmployeeServiceImpl( override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean = repository.existsByFirstNameAndLastName(firstName, lastName) - override fun getAll(): Collection = + override fun getAll(): Collection = 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 = + override fun getByGroup(group: Group): Collection = 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 +132,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.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 +153,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.update(entity) + return super.update(entity) } - override fun updatePassword(id: Long, password: String): Employee { - val persistedEmployee = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true) - return super.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 +209,11 @@ class EmployeeServiceImpl( }) } - override fun addPermission(employeeId: Long, permission: EmployeePermission): Employee = - super.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.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 +229,19 @@ class EmployeeServiceImpl( const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans @Service -class EmployeeGroupServiceImpl( - private val employeeService: EmployeeService, - employeeGroupRepository: EmployeeGroupRepository -) : AbstractExternalNamedModelService( - employeeGroupRepository +class GroupServiceImpl( + private val userService: UserService, + groupRepository: GroupRepository +) : AbstractExternalNamedModelService( + 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 +249,20 @@ class EmployeeGroupServiceImpl( ) override fun existsByName(name: String): Boolean = repository.existsByName(name) - override fun getEmployeesForGroup(id: Long): Collection = - employeeService.getByGroup(getById(id)) + override fun getUsersForGroup(id: Long): Collection = + userService.getByGroup(getById(id)) @Transactional - override fun save(entity: EmployeeGroup): EmployeeGroup { + override fun save(entity: Group): Group { return super.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 +271,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 +289,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 +298,13 @@ class EmployeeGroupServiceImpl( } @Service -class EmployeeUserDetailsServiceImpl( - private val employeeService: EmployeeService +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 +312,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) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index b170e81..7f0a1cc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -1,6 +1,7 @@ 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 @@ -38,7 +39,7 @@ class RecipeServiceImpl( val companyService: CompanyService, val mixService: MixService, val recipeStepService: RecipeStepService, - @Lazy val groupService: EmployeeGroupService, + @Lazy val groupService: GroupService, val recipeImageService: RecipeImageService ) : AbstractExternalModelService( @@ -157,7 +158,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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt index a72fc76..2475ed3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt @@ -2,6 +2,7 @@ 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 @@ -81,8 +82,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", diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt index a844d91..ba55ec5 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt @@ -4,15 +4,14 @@ 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 dev.fyloz.colorrecipesexplorer.model.account.* +import dev.fyloz.colorrecipesexplorer.repository.UserRepository +import dev.fyloz.colorrecipesexplorer.repository.GroupRepository 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 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 +21,25 @@ 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() { +class UserServiceTest : + AbstractExternalModelServiceTest() { 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 +52,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 +68,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 +79,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 { @@ -92,7 +92,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 { @@ -107,7 +107,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 +117,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 +128,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 +166,13 @@ class EmployeeServiceTest : } @Test - fun `save(dto) calls and returns save() with the created employee`() { - doReturn(entitySaveDtoEmployee).whenever(service).save(any()) + fun `save(dto) calls and returns save() with the created user`() { + doReturn(entitySaveDtoUser).whenever(service).save(any()) val found = service.save(entitySaveDto) - verify(service).save(argThat { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName }) - assertEquals(entitySaveDtoEmployee, found) + verify(service).save(argThat { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName }) + assertEquals(entitySaveDtoUser, found) } // update() @@ -182,7 +182,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 +198,47 @@ class EmployeeServiceTest : } } -class EmployeeGroupServiceTest : - AbstractExternalNamedModelServiceTest() { - private val employeeService: EmployeeService = mock() - override val repository: EmployeeGroupRepository = mock() - override val service: EmployeeGroupServiceImpl = spy(EmployeeGroupServiceImpl(employeeService, repository)) +class GroupServiceTest : + AbstractExternalNamedModelServiceTest() { + 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 +247,11 @@ class EmployeeGroupServiceTest : @Test fun `getRequestDefaultGroup() returns the group contained in the cookie of the HTTP request`() { - val cookies: Array = arrayOf(Cookie(defaultGroupCookieName, groupEmployeeId.toString())) + val cookies: Array = 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 +273,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 +281,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 +301,48 @@ class EmployeeGroupServiceTest : withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) } -class EmployeeUserDetailsServiceTest { - private val employeeService: EmployeeService = mock() - private val service = spy(EmployeeUserDetailsServiceImpl(employeeService)) +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 { service.loadUserByUsername(employee.id.toString()) } + assertThrows { 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) } } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt index 859f4d9..dd3793c 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt @@ -3,6 +3,7 @@ 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.* @@ -21,7 +22,7 @@ class RecipeServiceTest : 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())) @@ -129,9 +130,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( diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt index 48f476f..4fe2e5a 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt @@ -2,6 +2,7 @@ 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.assertThrows @@ -80,7 +81,7 @@ class RecipeStepServiceTest : private fun withGroupInformation(steps: MutableSet? = 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), From 42adb0ce9b8edd9b34ac4e654fcfa33c8be38777 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Thu, 6 May 2021 21:36:25 -0400 Subject: [PATCH 3/9] =?UTF-8?q?Mise=20=C3=A0=20jour=20vers=20Kotlin=201.5.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 48 +++++++++++-------- .../model/touchupkit/TouchUpKit.kt | 12 +++++ 2 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt diff --git a/build.gradle.kts b/build.gradle.kts index 5432c74..7538018 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,13 +2,20 @@ 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 { @@ -21,9 +28,9 @@ 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") @@ -31,23 +38,22 @@ dependencies { implementation("org.apache.pdfbox:pdfbox:2.0.4") implementation("dev.fyloz.colorrecipesexplorer:database-manager:1.2.0") - 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") @@ -90,8 +96,8 @@ tasks.withType().all { jvmTarget = JavaVersion.VERSION_11.toString() useIR = true freeCompilerArgs = listOf( - "-Xopt-in=kotlin.contracts.ExperimentalContracts", - "-Xinline-classes" + "-Xopt-in=kotlin.contracts.ExperimentalContracts", + "-Xinline-classes" ) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt new file mode 100644 index 0000000..7300624 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt @@ -0,0 +1,12 @@ +package dev.fyloz.colorrecipesexplorer.model.touchupkit + +data class TouchUpKit( + val id: Long, + val project: String, + val buggy: String +) + +sealed class TouchUpKitCompany { + inline class CompanyName(val name: String) + class Company(val company: Company) +} From cadf3dde8bef984cc87d8f4986a978679db1423d Mon Sep 17 00:00:00 2001 From: FyloZ Date: Wed, 19 May 2021 23:29:53 -0400 Subject: [PATCH 4/9] Ajout du support pour les kits de retouches (pas juste les PDFs) --- build.gradle.kts | 1 - .../annotations/PermissionAnnotations.kt | 12 - .../colorrecipesexplorer/model/Company.kt | 14 +- .../colorrecipesexplorer/model/Material.kt | 37 +-- .../model/MaterialType.kt | 16 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 30 +-- .../colorrecipesexplorer/model/MixMaterial.kt | 24 +- .../fyloz/colorrecipesexplorer/model/Model.kt | 5 + .../colorrecipesexplorer/model/Recipe.kt | 55 ++--- .../model/account/Group.kt | 20 +- .../model/account/Permission.kt | 30 +-- .../model/account/User.kt | 23 +- .../model/touchupkit/TouchUpKit.kt | 211 +++++++++++++++++- .../repository/TouchUpKitRepository.kt | 6 + .../rest/AccountControllers.kt | 5 +- .../rest/CompanyController.kt | 2 +- .../rest/MaterialController.kt | 2 +- .../rest/MaterialTypeController.kt | 2 +- .../rest/RecipeController.kt | 9 +- .../rest/TouchUpKitController.kt | 63 ++++++ .../rest/files/FileController.kt | 2 +- .../rest/files/TouchUpKitController.kt | 26 --- .../service/MaterialService.kt | 2 +- .../TouchUpKitService.kt | 55 ++++- src/main/resources/application.properties | 4 +- src/main/resources/junit-platform.properties | 1 - .../service/files/TouchUpKitServiceTest.kt | 7 +- 27 files changed, 427 insertions(+), 237 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/TouchUpKitRepository.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt delete mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/{files => touchupkit}/TouchUpKitService.kt (52%) delete mode 100644 src/main/resources/junit-platform.properties diff --git a/build.gradle.kts b/build.gradle.kts index 7538018..02b0630 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,6 @@ plugins { } repositories { - jcenter() mavenCentral() maven { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/PermissionAnnotations.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/PermissionAnnotations.kt index 04ff417..8f78572 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/PermissionAnnotations.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/annotations/PermissionAnnotations.kt @@ -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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt index c6b1fdf..d2bcba7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt @@ -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 { 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 { override fun toEntity(): Company = Company(id, name ?: "") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 5dc53de..76f505a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -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 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" diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt index c8f5e80..d19ac34 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt @@ -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 { override fun toEntity(): MaterialType = diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index e332b36..3622343 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -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? -) : EntityDto { - override fun toEntity(): Mix = throw UnsupportedOperationException() -} +) : EntityDto 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? -) : EntityDto { - override fun toEntity(): Mix = throw UnsupportedOperationException() -} +) : EntityDto 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? diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index 97f751c..c48316b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -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, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt index 2a50349..147285f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Model.kt @@ -15,3 +15,8 @@ interface EntityDto { 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" diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index cf0aad3..4f6551a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -5,8 +5,6 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.group -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 java.net.URLEncoder @@ -15,19 +13,7 @@ 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" @@ -91,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 { override fun toEntity(): Recipe = recipe( name = name, @@ -127,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?, @@ -188,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 ) data class RecipePublicDataDto( - @field:NotNull(message = RECIPE_ID_NULL_MESSAGE) val recipeId: Long, val notes: Set?, @@ -205,7 +185,6 @@ data class RecipePublicDataDto( ) data class NoteDto( - @field:NotNull(message = NOTE_GROUP_ID_NULL_MESSAGE) val groupId: Long, val content: String? @@ -290,8 +269,6 @@ private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found" private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists" private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe" -sealed class RecipeException - fun recipeIdNotFoundException(id: Long) = NotFoundException( RECIPE_EXCEPTION_ERROR_CODE, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt index 4bc229d..1169c5e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Group.kt @@ -3,21 +3,16 @@ 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.EntityDto -import dev.fyloz.colorrecipesexplorer.model.Model -import dev.fyloz.colorrecipesexplorer.model.NamedModel +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 -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 = "user_group") data class Group( @@ -43,11 +38,10 @@ data class Group( } open class GroupSaveDto( - @field:NotBlank(message = GROUP_NAME_NULL_MESSAGE) - @field:Size(min = 3) + @field:NotBlank val name: String, - @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE) + @field:NotEmpty val permissions: MutableSet ) : EntityDto { override fun toEntity(): Group = @@ -55,14 +49,12 @@ open class GroupSaveDto( } open class GroupUpdateDto( - @field:NotNull(message = GROUP_ID_NULL_MESSAGE) val id: Long, - @field:NotBlank(message = GROUP_NAME_NULL_MESSAGE) - @field:Size(min = 3) + @field:NotBlank val name: String, - @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE) + @field:NotEmpty val permissions: MutableSet ) : EntityDto { override fun toEntity(): Group = diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Permission.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Permission.kt index 333f8fb..abd2fb0 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Permission.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/Permission.kt @@ -9,14 +9,11 @@ enum class Permission( ) { 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 Permission( 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 Permission( 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), diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt index c6e2b58..3f4a64a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/account/User.kt @@ -4,7 +4,6 @@ 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.NullOrNotBlank import org.hibernate.annotations.Fetch import org.hibernate.annotations.FetchMode import org.springframework.security.core.GrantedAuthority @@ -13,14 +12,9 @@ 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 USER_ID_NULL_MESSAGE = "Un numéro d'utilisateur est requis" -private const val USER_LAST_NAME_EMPTY_MESSAGE = "Un nom est requis" -private const val USER_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis" -private const val USER_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis" -private const val USER_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères" +private const val VALIDATION_PASSWORD_LENGTH = "Must contains at least 8 characters" @Entity @Table(name = "user") @@ -70,19 +64,17 @@ data class User( get() = flatPermissions.map { it.toAuthority() }.toMutableSet() } -/** DTO for creating users. Allows a [password] a [groupId]. */ open class UserSaveDto( - @field:NotNull(message = USER_ID_NULL_MESSAGE) val id: Long, - @field:NotBlank(message = USER_FIRST_NAME_EMPTY_MESSAGE) + @field:NotBlank val firstName: String, - @field:NotBlank(message = USER_LAST_NAME_EMPTY_MESSAGE) + @field:NotBlank val lastName: String, - @field:NotBlank(message = USER_PASSWORD_EMPTY_MESSAGE) - @field:Size(min = 8, message = USER_PASSWORD_TOO_SHORT_MESSAGE) + @field:NotBlank + @field:Size(min = 8, message = VALIDATION_PASSWORD_LENGTH) val password: String, val groupId: Long?, @@ -92,13 +84,12 @@ open class UserSaveDto( ) : EntityDto open class UserUpdateDto( - @field:NotNull(message = USER_ID_NULL_MESSAGE) val id: Long, - @field:NullOrNotBlank(message = USER_FIRST_NAME_EMPTY_MESSAGE) + @field:NotBlank val firstName: String?, - @field:NullOrNotBlank(message = USER_LAST_NAME_EMPTY_MESSAGE) + @field:NotBlank val lastName: String?, val groupId: Long?, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt index 7300624..e6a01ed 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/touchupkit/TouchUpKit.kt @@ -1,12 +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( - val id: Long, + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + override val id: Long?, + val project: String, - val buggy: 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 +) : 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, + + @field:NotEmpty + val material: List, + + @field:NotEmpty + val content: Set +) : EntityDto { + 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?, + + @field:NotEmpty + val material: List?, + + @field:NotEmpty + val content: Set? +) : EntityDto + +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, + val material: List, + val content: Set, + val pdfUrl: String +) : Model + +data class TouchUpKitProductDto( + val name: String, + val description: String?, + val quantity: Float ) -sealed class TouchUpKitCompany { - inline class CompanyName(val name: String) - class Company(val company: Company) -} +// ==== DSL ==== +fun touchUpKit( + id: Long? = null, + project: String = "project", + buggy: String = "buggy", + company: String = "company", + quantity: Int = 1, + shippingDate: LocalDate = LocalDate.now(), + finish: List, + material: List, + content: Set, + 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 + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/TouchUpKitRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/TouchUpKitRepository.kt new file mode 100644 index 0000000..0819613 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/TouchUpKitRepository.kt @@ -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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt index 62f6d03..69165f4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt @@ -1,7 +1,6 @@ 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.account.* import dev.fyloz.colorrecipesexplorer.service.UserService @@ -87,7 +86,7 @@ class UserController(private val userService: UserService) { } @DeleteMapping("{id}") - @PreAuthorizeRemoveUsers + @PreAuthorizeEditUsers fun deleteById(@PathVariable id: Long) = userService.deleteById(id) } @@ -147,7 +146,7 @@ class GroupsController( } @DeleteMapping("{id}") - @PreAuthorizeRemoveUsers + @PreAuthorizeEditUsers fun deleteById(@PathVariable id: Long) = noContent { groupService.deleteById(id) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt index e1acc00..f16f253 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt @@ -38,7 +38,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) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt index 8e99b03..0bec46a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt @@ -64,7 +64,7 @@ class MaterialController( } @DeleteMapping("{id}") - @PreAuthorize("hasAuthority('REMOVE_MATERIALS')") + @PreAuthorize("hasAuthority('EDIT_MATERIALS')") fun deleteById(@PathVariable id: Long) = noContent { materialService.deleteById(id) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt index 50e26f0..877f8d1 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt @@ -38,7 +38,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) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index 8bf447e..efdd414 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -1,11 +1,8 @@ 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 @@ -14,8 +11,6 @@ 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 @@ -61,7 +56,7 @@ class RecipeController( } @DeleteMapping("{id}") - @PreAuthorizeRemoveRecipes + @PreAuthorizeEditRecipes fun deleteById(@PathVariable id: Long) = noContent { recipeService.deleteById(id) @@ -105,7 +100,7 @@ class MixController(private val mixService: MixService) { } @DeleteMapping("{id}") - @PreAuthorizeRemoveRecipes + @PreAuthorizeEditRecipes fun deleteById(@PathVariable id: Long) = noContent { mixService.deleteById(id) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt new file mode 100644 index 0000000..c39684d --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt @@ -0,0 +1,63 @@ +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.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) +@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(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 { + with(touchUpKitService.generateJobPdfResource(project)) { + return ResponseEntity.ok() + .header("Content-Disposition", "filename=TouchUpKit_$project.pdf") + .contentLength(this.contentLength()) + .contentType(MediaType.APPLICATION_PDF) + .body(this) + } + } +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt index 9e5125b..cad230a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt @@ -46,7 +46,7 @@ class FileController( } @DeleteMapping - @PreAuthorize("hasAnyAuthority('REMOVE_FILE')") + @PreAuthorize("hasAnyAuthority('WRITE_FILE')") fun delete(@RequestParam path: String): ResponseEntity { return noContent { fileService.delete(path) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt deleted file mode 100644 index 6993025..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/TouchUpKitController.kt +++ /dev/null @@ -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 { - with(touchUpKitService.generateJobPdfResource(job)) { - return ResponseEntity.ok() - .header("Content-Disposition", "filename=TouchUpKit_$job.pdf") - .contentLength(this.contentLength()) - .contentType(MediaType.APPLICATION_PDF) - .body(this) - } - } -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt index 02b843d..cb5c3ab 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -136,7 +136,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) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/touchupkit/TouchUpKitService.kt similarity index 52% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/touchupkit/TouchUpKitService.kt index 68b043e..79c9440 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/touchupkit/TouchUpKitService.kt @@ -1,6 +1,12 @@ -package dev.fyloz.colorrecipesexplorer.service.files +package dev.fyloz.colorrecipesexplorer.service.touchupkit import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties +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.ExternalModelService +import dev.fyloz.colorrecipesexplorer.service.files.FileService import dev.fyloz.colorrecipesexplorer.utils.* import org.springframework.core.io.ByteArrayResource import org.springframework.stereotype.Service @@ -10,7 +16,8 @@ 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 { +interface TouchUpKitService : + ExternalModelService { /** Generates and returns a [PdfDocument] for the given [job]. */ fun generateJobPdf(job: String): PdfDocument @@ -29,8 +36,45 @@ interface TouchUpKitService { @Service class TouchUpKitServiceImpl( private val fileService: FileService, - private val creProperties: CreProperties -) : TouchUpKitService { + touchUpKitRepository: TouchUpKitRepository, + private val creProperties: CreProperties, +) : AbstractExternalModelService( + 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 @@ -75,4 +119,7 @@ class TouchUpKitServiceImpl( private fun String.pdfDocumentPath() = "$TOUCH_UP_KIT_FILES_PATH/$this.pdf" + + private fun TouchUpKit.pdfUrl() = + "${creProperties.deploymentUrl}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project" } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 74dd17a..d6fad50 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -21,7 +21,7 @@ entities.material-types.baseName=Base 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 +30,6 @@ 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@ diff --git a/src/main/resources/junit-platform.properties b/src/main/resources/junit-platform.properties deleted file mode 100644 index 2af5bf8..0000000 --- a/src/main/resources/junit-platform.properties +++ /dev/null @@ -1 +0,0 @@ -junit.jupiter.testinstance.lifecycle.default=per_class diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt index 4affc53..3813fd5 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt @@ -1,6 +1,10 @@ package dev.fyloz.colorrecipesexplorer.service.files import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties +import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository +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 +14,14 @@ import org.springframework.core.io.ByteArrayResource import kotlin.test.assertEquals private class TouchUpKitServiceTestContext { + val touchUpKitRepository = mockk() val fileService = mockk { every { write(any(), any(), any()) } just Runs } val creProperties = mockk { every { cacheGeneratedFiles } returns false } - val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, creProperties)) + val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, touchUpKitRepository, creProperties)) val pdfDocumentData = mockk() val pdfDocument = mockk { mockkStatic(PdfDocument::toByteArrayResource) From 5b3524dae91c9b298a3775f86f70ab3a8430f9c1 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Thu, 20 May 2021 13:08:28 -0400 Subject: [PATCH 5/9] =?UTF-8?q?Ajout=20d'un=20endpoint=20pour=20r=C3=A9cup?= =?UTF-8?q?=C3=A9rer=20les=20couleurs=20selon=20leur=20nom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/RecipeRepository.kt | 3 +++ .../colorrecipesexplorer/rest/RecipeController.kt | 9 +++++++-- .../colorrecipesexplorer/service/RecipeService.kt | 6 +++++- .../service/AccountsServiceTest.kt | 8 ++++---- .../service/CompanyServiceTest.kt | 2 ++ .../service/InventoryServiceTest.kt | 2 ++ .../service/MaterialServiceTest.kt | 2 ++ .../service/MaterialTypeServiceTest.kt | 2 ++ .../service/MixMaterialServiceTest.kt | 2 ++ .../service/MixServiceTest.kt | 2 ++ .../service/MixTypeServiceTest.kt | 2 ++ .../service/RecipeServiceTest.kt | 15 +++++++++++++++ .../service/RecipeStepServiceTest.kt | 2 ++ 13 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt index a7284f7..251374d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt @@ -11,6 +11,9 @@ interface RecipeRepository : JpaRepository { /** 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 + /** Gets all recipes with the given [company]. */ fun findAllByCompany(company: Company): Collection } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index efdd414..0b840a9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -25,8 +25,13 @@ class RecipeController( 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) = diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index 7f0a1cc..c28ef6e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -20,6 +20,9 @@ interface RecipeService : /** 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 + /** Gets all recipes with the given [company]. */ fun getAllByCompany(company: Company): Collection @@ -74,7 +77,8 @@ class RecipeServiceImpl( override fun existsByNameAndCompany(name: String, company: Company) = repository.existsByNameAndCompany(name, company) - override fun getAllByCompany(company: Company): Collection = repository.findAllByCompany(company) + override fun getAllByName(name: String) = repository.findAllByName(name) + override fun getAllByCompany(company: Company) = repository.findAllByCompany(company) override fun save(entity: RecipeSaveDto): Recipe { val company = companyService.getById(entity.companyId) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt index ba55ec5..4d15164 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt @@ -7,10 +7,7 @@ import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.* import dev.fyloz.colorrecipesexplorer.repository.UserRepository import dev.fyloz.colorrecipesexplorer.repository.GroupRepository -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 org.junit.jupiter.api.* import org.springframework.mock.web.MockHttpServletResponse import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder @@ -23,6 +20,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import org.springframework.security.core.userdetails.User as SpringUser +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class UserServiceTest : AbstractExternalModelServiceTest() { private val passwordEncoder = BCryptPasswordEncoder() @@ -198,6 +196,7 @@ class UserServiceTest : } } +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class GroupServiceTest : AbstractExternalNamedModelServiceTest() { private val userService: UserService = mock() @@ -301,6 +300,7 @@ class GroupServiceTest : withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() }) } +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class UserUserDetailsServiceTest { private val userService: UserService = mock() private val service = spy(CreUserDetailsServiceImpl(userService)) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyServiceTest.kt index 75dfba1..7843cae 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyServiceTest.kt @@ -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() { private val recipeService: RecipeService = mock() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt index aae3b92..98eb32e 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt @@ -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() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt index dc13ca3..9b3dacb 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt @@ -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() { override val repository: MaterialRepository = mock() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt index 797c1d0..7d8a1dd 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt @@ -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() { override val repository: MaterialTypeRepository = mock() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialServiceTest.kt index bffc6a4..0ab18cf 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialServiceTest.kt @@ -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() { override val repository: MixMaterialRepository = mock() private val materialService: MaterialService = mock() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixServiceTest.kt index 00d1c2c..a9debc1 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixServiceTest.kt @@ -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() { override val repository: MixRepository = mock() private val recipeService: RecipeService = mock() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt index b57d4ce..4e6f246 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt @@ -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() { override val repository: MixTypeRepository = mock() private val materialService: MaterialService = mock() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt index dd3793c..a8ccd0b 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt @@ -9,6 +9,7 @@ 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 @@ -17,6 +18,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class RecipeServiceTest : AbstractExternalModelServiceTest() { override val repository: RecipeRepository = mock() @@ -72,6 +74,19 @@ class RecipeServiceTest : } } + // 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 diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt index 4fe2e5a..b0f9c73 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepServiceTest.kt @@ -5,9 +5,11 @@ 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() { override val repository: RecipeStepRepository = mock() From b43f7ca357e7b8267a146fd605d9c209b97b1057 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Thu, 20 May 2021 13:37:31 -0400 Subject: [PATCH 6/9] =?UTF-8?q?Mise=20=C3=A0=20jour=20du=20gestionnaire=20?= =?UTF-8?q?de=20base=20de=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- .../DatabaseVersioning.kt | 2 +- .../model/Configuration.kt | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt diff --git a/build.gradle.kts b/build.gradle.kts index 02b0630..e437730 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,7 +35,7 @@ dependencies { 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:${springBootVersion}") implementation("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt index 81d6419..fae5ad8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt @@ -11,7 +11,7 @@ import org.springframework.context.annotation.Configuration import org.springframework.core.env.Environment 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+)$") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt new file mode 100644 index 0000000..951db30 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt @@ -0,0 +1,20 @@ +package dev.fyloz.colorrecipesexplorer.model + +import java.time.LocalDateTime +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Table + +@Entity +@Table(name = "configuration") +data class Configuration( + @Id + @Column(name = "config_key") + val key: String, + + val content: String, + + @Column(name = "last_updated") + val lastUpdated: LocalDateTime +) From f376d66b2e29b5dd200fe14091f56b79f0694567 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Fri, 28 May 2021 15:35:25 -0400 Subject: [PATCH 7/9] Ajout des configurations et du mode "emergency" --- .gitignore | 2 + build.gradle.kts | 2 + buildversion | 1 + .../service/files/XlsService.java | 2 + .../ColorRecipesExplorerApplication.kt | 37 +++- .../DatabaseVersioning.kt | 99 +++++++--- .../config/ApplicationReadyListener.kt | 29 ++- .../config/ConfigurationsInitializer.kt | 54 ++++++ .../config/EmergencySecurityConfig.kt | 95 ++++++++++ .../config/SpringConfiguration.kt | 5 + .../config/WebSecurityConfig.kt | 171 +++++++++--------- .../model/Configuration.kt | 119 +++++++++++- .../colorrecipesexplorer/model/Recipe.kt | 6 +- .../repository/ConfigurationRepository.kt | 7 + .../rest/AccountControllers.kt | 6 +- .../rest/CompanyController.kt | 2 + .../rest/ConfigurationController.kt | 56 ++++++ .../rest/{files => }/FileController.kt | 11 +- .../rest/InventoryController.kt | 2 + .../rest/MaterialController.kt | 2 + .../rest/MaterialTypeController.kt | 2 + .../rest/RecipeController.kt | 3 + .../colorrecipesexplorer/rest/RestUtils.kt | 2 +- .../rest/TouchUpKitController.kt | 2 + .../service/AccountService.kt | 4 + .../service/CompanyService.kt | 2 + .../service/ConfigurationService.kt | 155 ++++++++++++++++ .../service/InventoryService.kt | 2 + .../service/MaterialService.kt | 10 +- .../service/MaterialTypeService.kt | 2 + .../service/MixMaterialService.kt | 2 + .../service/MixService.kt | 2 + .../service/MixTypeService.kt | 2 + .../service/RecipeService.kt | 8 +- .../service/RecipeStepService.kt | 2 + .../service/touchupkit/TouchUpKitService.kt | 17 +- .../application-emergency.properties | 1 + .../resources/application-mysql.properties | 9 +- src/main/resources/application.properties | 5 +- 39 files changed, 789 insertions(+), 151 deletions(-) create mode 100644 buildversion create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ConfigurationsInitializer.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/EmergencySecurityConfig.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/ConfigurationRepository.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt rename src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/{files => }/FileController.kt (84%) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationService.kt create mode 100644 src/main/resources/application-emergency.properties diff --git a/.gitignore b/.gitignore index a57ac6a..d512c1f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ dist/ out/ /src/main/resources/angular/static/* + +config.properties diff --git a/build.gradle.kts b/build.gradle.kts index e437730..bd9f8dd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,6 +58,8 @@ dependencies { runtimeOnly("mysql:mysql-connector-java:8.0.22") runtimeOnly("org.postgresql:postgresql:42.2.16") runtimeOnly("com.microsoft.sqlserver:mssql-jdbc:9.2.1.jre11") + + implementation("org.springframework.cloud:spring-cloud-starter:2.2.8.RELEASE") } java { diff --git a/buildversion b/buildversion new file mode 100644 index 0000000..3a2e3f4 --- /dev/null +++ b/buildversion @@ -0,0 +1 @@ +-1 diff --git a/src/main/java/dev/fyloz/colorrecipesexplorer/service/files/XlsService.java b/src/main/java/dev/fyloz/colorrecipesexplorer/service/files/XlsService.java index 50553b6..812bd0a 100644 --- a/src/main/java/dev/fyloz/colorrecipesexplorer/service/files/XlsService.java +++ b/src/main/java/dev/fyloz/colorrecipesexplorer/service/files/XlsService.java @@ -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; diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/ColorRecipesExplorerApplication.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/ColorRecipesExplorerApplication.kt index 17c867a..13cef5b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/ColorRecipesExplorerApplication.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/ColorRecipesExplorerApplication.kt @@ -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() + 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() diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt index fae5ad8..9b0411a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt @@ -1,37 +1,81 @@ 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 javax.sql.DataSource const val SUPPORTED_DATABASE_VERSION = 5 const val ENV_VAR_ENABLE_DATABASE_UPDATE_NAME = "CRE_ENABLE_DB_UPDATE" val DATABASE_NAME_REGEX = Regex("(\\w+)$") +@Profile("!emergency") @Configuration +@DependsOn("configurationsInitializer") class DataSourceConfiguration { @Bean(name = ["dataSource"]) - @ConfigurationProperties(prefix = "spring.datasource") fun customDataSource( - logger: Logger, - environment: Environment, - databaseUpdaterProperties: DatabaseUpdaterProperties + logger: Logger, + environment: ConfigurableEnvironment, + fileConfiguration: FileConfiguration, + databaseUpdaterProperties: DatabaseUpdaterProperties ): DataSource { - val databaseUrl: String = environment.getProperty("spring.datasource.url")!! + fun getConfiguration(type: ConfigurationType, defaultProperty: String) = + fileConfiguration.get(type)?.content ?: defaultProperty - runDatabaseVersionCheck(logger, databaseUrl, databaseUpdaterProperties) + val databaseUrl = "jdbc:" + getConfiguration(ConfigurationType.DATABASE_URL, databaseUpdaterProperties.url) + val databaseUsername = getConfiguration(ConfigurationType.DATABASE_USER, databaseUpdaterProperties.username) + val databasePassword = getConfiguration(ConfigurationType.DATABASE_PASSWORD, databaseUpdaterProperties.password) + + try { + runDatabaseVersionCheck(logger, databaseUrl, DatabaseUpdaterProperties().apply { + url = databaseUrl + username = databaseUsername + password = databasePassword + }) + } catch (ex: CreDatabaseException) { + 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 +119,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 +145,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 +157,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 +167,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") } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ApplicationReadyListener.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ApplicationReadyListener.kt index f135661..f70db75 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ApplicationReadyListener.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ApplicationReadyListener.kt @@ -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 { 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 { + override fun onApplicationEvent(event: ApplicationEnvironmentPreparedEvent) { + if (emergencyMode) { + event.environment.setActiveProfiles("emergency") + } + } +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ConfigurationsInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ConfigurationsInitializer.kt new file mode 100644 index 0000000..28b92f6 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/ConfigurationsInitializer.kt @@ -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" +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/EmergencySecurityConfig.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/EmergencySecurityConfig.kt new file mode 100644 index 0000000..b944b96 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/EmergencySecurityConfig.kt @@ -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()) + } +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/SpringConfiguration.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/SpringConfiguration.kt index 5dc9eb5..ed053e7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/SpringConfiguration.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/SpringConfiguration.kt @@ -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) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt index 1fb70ea..80e0ae0 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt @@ -1,14 +1,13 @@ package dev.fyloz.colorrecipesexplorer.config import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.fyloz.colorrecipesexplorer.emergencyMode import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest import dev.fyloz.colorrecipesexplorer.model.account.Permission import dev.fyloz.colorrecipesexplorer.model.account.User -import dev.fyloz.colorrecipesexplorer.service.UserService -import dev.fyloz.colorrecipesexplorer.service.UserServiceImpl +import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsService -import dev.fyloz.colorrecipesexplorer.service.CreUserDetailsServiceImpl +import dev.fyloz.colorrecipesexplorer.service.UserService import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm @@ -18,6 +17,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Lazy +import org.springframework.context.annotation.Profile import org.springframework.core.env.Environment import org.springframework.http.HttpMethod import org.springframework.security.authentication.AuthenticationManager @@ -31,7 +31,7 @@ import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.core.Authentication import org.springframework.security.core.AuthenticationException import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.core.userdetails.User as SpringUser +import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter @@ -46,15 +46,17 @@ import javax.annotation.PostConstruct import javax.servlet.FilterChain import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse +import org.springframework.security.core.userdetails.User as SpringUser @Configuration +@Profile("!emergency") @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableConfigurationProperties(SecurityConfigurationProperties::class) class WebSecurityConfig( val securityConfigurationProperties: SecurityConfigurationProperties, - @Lazy val userDetailsService: CreUserDetailsServiceImpl, - @Lazy val userService: UserServiceImpl, + @Lazy val userDetailsService: CreUserDetailsService, + @Lazy val userService: UserService, val environment: Environment, val logger: Logger ) : WebSecurityConfigurerAdapter() { @@ -66,51 +68,56 @@ class WebSecurityConfig( @Bean fun passwordEncoder() = - BCryptPasswordEncoder() + BCryptPasswordEncoder() @Bean override fun authenticationManagerBean(): AuthenticationManager = - super.authenticationManagerBean() + super.authenticationManagerBean() @Bean fun corsConfigurationSource() = - UrlBasedCorsConfigurationSource().apply { - registerCorsConfiguration("/**", CorsConfiguration().apply { - allowedOrigins = listOf("http://localhost:4200") // Angular development server - allowedMethods = listOf( - HttpMethod.GET.name, - HttpMethod.POST.name, - HttpMethod.PUT.name, - HttpMethod.DELETE.name, - HttpMethod.OPTIONS.name, - HttpMethod.HEAD.name - ) - allowCredentials = true - }.applyPermitDefaultValues()) - } + UrlBasedCorsConfigurationSource().apply { + registerCorsConfiguration("/**", CorsConfiguration().apply { + allowedOrigins = listOf("http://localhost:4200") // Angular development server + allowedMethods = listOf( + HttpMethod.GET.name, + HttpMethod.POST.name, + HttpMethod.PUT.name, + HttpMethod.DELETE.name, + HttpMethod.OPTIONS.name, + HttpMethod.HEAD.name + ) + allowCredentials = true + }.applyPermitDefaultValues()) + } @PostConstruct fun initWebSecurity() { fun createUser( - credentials: SecurityConfigurationProperties.SystemUserCredentials?, - firstName: String, - lastName: String, - permissions: List + credentials: SecurityConfigurationProperties.SystemUserCredentials?, + firstName: String, + lastName: String, + permissions: List ) { + if (emergencyMode) { + logger.error("Emergency mode is enabled, root user will not be created") + return + } + Assert.notNull(credentials, "No root user has been defined.") credentials!! Assert.notNull(credentials.id, "The root user has no identifier defined.") Assert.notNull(credentials.password, "The root user has no password defined.") if (!userService.existsById(credentials.id!!)) { userService.save( - User( - id = credentials.id!!, - firstName = firstName, - lastName = lastName, - password = passwordEncoder().encode(credentials.password!!), - isSystemUser = true, - permissions = permissions.toMutableSet() - ) + User( + id = credentials.id!!, + firstName = firstName, + lastName = lastName, + password = passwordEncoder().encode(credentials.password!!), + isSystemUser = true, + permissions = permissions.toMutableSet() + ) ) } } @@ -122,37 +129,35 @@ class WebSecurityConfig( override fun configure(http: HttpSecurity) { http - .headers().frameOptions().disable() - .and() - .csrf().disable() - .addFilter( - JwtAuthenticationFilter( - authenticationManager(), - userService, - securityConfigurationProperties + .headers().frameOptions().disable() + .and() + .csrf().disable() + .addFilter( + JwtAuthenticationFilter( + authenticationManager(), + securityConfigurationProperties + ) { userService.updateLastLoginTime(it) } ) - ) - .addFilter( - JwtAuthorizationFilter( - userDetailsService, - securityConfigurationProperties, - authenticationManager() + .addFilter( + JwtAuthorizationFilter( + securityConfigurationProperties, + authenticationManager() + ) { userDetailsService.loadUserById(it, false) } ) - ) - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) if (!debugMode) { http.authorizeRequests() - .antMatchers("/api/login").permitAll() - .antMatchers("/api/logout").authenticated() - .antMatchers("/api/user/current").authenticated() - .anyRequest().authenticated() + .antMatchers("/api/login").permitAll() + .antMatchers("/api/logout").authenticated() + .antMatchers("/api/user/current").authenticated() + .anyRequest().authenticated() } else { http - .cors() - .and() - .authorizeRequests() - .antMatchers("**").permitAll() + .cors() + .and() + .authorizeRequests() + .antMatchers("**").permitAll() } } } @@ -160,9 +165,9 @@ class WebSecurityConfig( @Component class RestAuthenticationEntryPoint : AuthenticationEntryPoint { override fun commence( - request: HttpServletRequest, - response: HttpServletResponse, - authException: AuthenticationException + request: HttpServletRequest, + response: HttpServletResponse, + authException: AuthenticationException ) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized") } @@ -172,8 +177,8 @@ val blacklistedJwtTokens = mutableListOf() class JwtAuthenticationFilter( private val authManager: AuthenticationManager, - private val userService: UserService, - private val securityConfigurationProperties: SecurityConfigurationProperties + private val securityConfigurationProperties: SecurityConfigurationProperties, + private val updateUserLoginTime: (Long) -> Unit ) : UsernamePasswordAuthenticationFilter() { private var debugMode = false @@ -188,31 +193,31 @@ class JwtAuthenticationFilter( } override fun successfulAuthentication( - request: HttpServletRequest, - response: HttpServletResponse, - chain: FilterChain, - authResult: Authentication + request: HttpServletRequest, + response: HttpServletResponse, + chain: FilterChain, + authResult: Authentication ) { val jwtSecret = securityConfigurationProperties.jwtSecret val jwtDuration = securityConfigurationProperties.jwtDuration Assert.notNull(jwtSecret, "No JWT secret has been defined.") Assert.notNull(jwtDuration, "No JWT duration has been defined.") val userId = (authResult.principal as SpringUser).username - userService.updateLastLoginTime(userId.toLong()) + updateUserLoginTime(userId.toLong()) val expirationMs = System.currentTimeMillis() + jwtDuration!! val expirationDate = Date(expirationMs) val token = Jwts.builder() - .setSubject(userId) - .setExpiration(expirationDate) - .signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray()) - .compact() + .setSubject(userId) + .setExpiration(expirationDate) + .signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray()) + .compact() response.addHeader("Access-Control-Expose-Headers", "X-Authentication-Expiration") var bearerCookie = - "$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; SameSite=strict" + "$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; SameSite=strict" if (!debugMode) bearerCookie += "; Secure;" response.addHeader( - "Set-Cookie", - bearerCookie + "Set-Cookie", + bearerCookie ) response.addHeader(authorizationCookieName, "Bearer $token") response.addHeader("X-Authentication-Expiration", "$expirationMs") @@ -220,9 +225,9 @@ class JwtAuthenticationFilter( } class JwtAuthorizationFilter( - private val userDetailsService: CreUserDetailsService, private val securityConfigurationProperties: SecurityConfigurationProperties, - authenticationManager: AuthenticationManager + authenticationManager: AuthenticationManager, + private val loadUserById: (Long) -> UserDetails ) : BasicAuthenticationFilter(authenticationManager) { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { fun tryLoginFromBearer(): Boolean { @@ -260,10 +265,10 @@ class JwtAuthorizationFilter( Assert.notNull(jwtSecret, "No JWT secret has been defined.") return try { val userId = Jwts.parser() - .setSigningKey(jwtSecret!!.toByteArray()) - .parseClaimsJws(token.replace("Bearer", "")) - .body - .subject + .setSigningKey(jwtSecret!!.toByteArray()) + .parseClaimsJws(token.replace("Bearer", "")) + .body + .subject if (userId != null) getAuthenticationToken(userId) else null } catch (_: ExpiredJwtException) { null @@ -271,7 +276,7 @@ class JwtAuthorizationFilter( } private fun getAuthenticationToken(userId: String): UsernamePasswordAuthenticationToken? = try { - val userDetails = userDetailsService.loadUserById(userId.toLong(), false) + val userDetails = loadUserById(userId.toLong()) UsernamePasswordAuthenticationToken(userDetails.username, null, userDetails.authorities) } catch (_: NotFoundException) { null diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt index 951db30..e98a938 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt @@ -1,14 +1,33 @@ package dev.fyloz.colorrecipesexplorer.model +import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.exception.RestException +import org.springframework.http.HttpStatus +import org.springframework.web.multipart.MultipartFile import java.time.LocalDateTime import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id import javax.persistence.Table +import javax.validation.constraints.NotBlank + +data class Configuration( + @JsonIgnore + val type: ConfigurationType, + val content: String, + val lastUpdated: LocalDateTime +) { + val key = type.key + val requireRestart = type.requireRestart + val editable = !type.computed + + fun toEntity() = + ConfigurationEntity(key, content, lastUpdated) +} @Entity @Table(name = "configuration") -data class Configuration( +data class ConfigurationEntity( @Id @Column(name = "config_key") val key: String, @@ -17,4 +36,102 @@ data class Configuration( @Column(name = "last_updated") val lastUpdated: LocalDateTime +) { + fun toConfiguration() = + configuration(key.toConfigurationType(), content, lastUpdated) +} + +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 + ) + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index 4f6551a..fbbe87f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -6,7 +6,7 @@ import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.group import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES -import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH +import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.time.LocalDate @@ -67,8 +67,8 @@ data class Recipe( fun groupInformationForGroup(groupId: Long) = groupsInformation.firstOrNull { it.group.id == groupId } - fun imageUrl(name: String) = - "${CRE_PROPERTIES.deploymentUrl}$FILE_CONTROLLER_PATH?path=${ + fun imageUrl(deploymentUrl: String, name: String) = + "$deploymentUrl$FILE_CONTROLLER_PATH?path=${ URLEncoder.encode( "${this.imagesDirectoryPath}/$name", StandardCharsets.UTF_8 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/ConfigurationRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/ConfigurationRepository.kt new file mode 100644 index 0000000..99ac6d2 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/ConfigurationRepository.kt @@ -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 { +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt index 69165f4..52e61d0 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt @@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers import dev.fyloz.colorrecipesexplorer.model.account.* import dev.fyloz.colorrecipesexplorer.service.UserService import dev.fyloz.colorrecipesexplorer.service.GroupService +import org.springframework.context.annotation.Profile import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* @@ -18,6 +19,7 @@ private const val GROUP_CONTROLLER_PATH = "api/user/group" @RestController @RequestMapping(USER_CONTROLLER_PATH) +@Profile("!emergency") class UserController(private val userService: UserService) { @GetMapping @PreAuthorizeViewUsers @@ -93,6 +95,7 @@ class UserController(private val userService: UserService) { @RestController @RequestMapping(GROUP_CONTROLLER_PATH) +@Profile("!emergency") class GroupsController( private val groupService: GroupService, private val userService: UserService @@ -155,10 +158,11 @@ class GroupsController( @RestController @RequestMapping("api") +@Profile("!emergency") class LogoutController(private val userService: UserService) { @GetMapping("logout") fun logout(request: HttpServletRequest) = - ok { + ok { userService.logout(request) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt index f16f253..77704da 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt @@ -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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt new file mode 100644 index 0000000..a3017c6 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/ConfigurationController.kt @@ -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) = 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 +} + diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt similarity index 84% rename from src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt rename to src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt index cad230a..e91186c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/FileController.kt @@ -1,7 +1,7 @@ -package dev.fyloz.colorrecipesexplorer.rest.files +package dev.fyloz.colorrecipesexplorer.rest -import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties -import dev.fyloz.colorrecipesexplorer.rest.noContent +import dev.fyloz.colorrecipesexplorer.model.ConfigurationType +import dev.fyloz.colorrecipesexplorer.service.ConfigurationService import dev.fyloz.colorrecipesexplorer.service.files.FileService import org.springframework.core.io.ByteArrayResource import org.springframework.http.MediaType @@ -18,10 +18,9 @@ private const val DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE @RequestMapping(FILE_CONTROLLER_PATH) class FileController( private val fileService: FileService, - private val creProperties: CreProperties + private val configService: ConfigurationService ) { @GetMapping(produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) - @PreAuthorize("hasAnyAuthority('READ_FILE')") fun upload( @RequestParam path: String, @RequestParam(required = false) mediaType: String? @@ -55,7 +54,7 @@ class FileController( private fun created(path: String): ResponseEntity = 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) = diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt index 636c3af..abf6d49 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt @@ -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 ) { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt index 0bec46a..e5d13f9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt @@ -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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt index 877f8d1..a8ff9bd 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt @@ -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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index 0b840a9..9d601ba 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -6,6 +6,7 @@ import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.service.MixService import dev.fyloz.colorrecipesexplorer.service.RecipeImageService import dev.fyloz.colorrecipesexplorer.service.RecipeService +import org.springframework.context.annotation.Profile import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize @@ -19,6 +20,7 @@ private const val MIX_CONTROLLER_PATH = "api/recipe/mix" @RestController @RequestMapping(RECIPE_CONTROLLER_PATH) +@Profile("!emergency") @PreAuthorizeViewRecipes class RecipeController( private val recipeService: RecipeService, @@ -84,6 +86,7 @@ class RecipeController( @RestController @RequestMapping(MIX_CONTROLLER_PATH) +@Profile("!emergency") @PreAuthorizeViewRecipes class MixController(private val mixService: MixService) { @GetMapping("{id}") diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt index c7a82fc..23d59da 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt @@ -19,7 +19,7 @@ fun ok(body: T, headers: HttpHeaders): ResponseEntity = ResponseEntity(body, headers, HttpStatus.OK) /** Executes the given [action] then returns an HTTP OK [ResponseEntity] form the given [body]. */ -fun ok(action: () -> Unit): ResponseEntity { +fun ok(action: () -> Unit): ResponseEntity { action() return ResponseEntity.ok().build() } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt index c39684d..8e27621 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/TouchUpKitController.kt @@ -4,6 +4,7 @@ import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitOutputDto import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitSaveDto import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitUpdateDto import dev.fyloz.colorrecipesexplorer.service.touchupkit.TouchUpKitService +import org.springframework.context.annotation.Profile import org.springframework.core.io.ByteArrayResource import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -15,6 +16,7 @@ const val TOUCH_UP_KIT_CONTROLLER_PATH = "/api/touchupkit" @RestController @RequestMapping(TOUCH_UP_KIT_CONTROLLER_PATH) +@Profile("!emergency") @PreAuthorize("hasAuthority('VIEW_TOUCH_UP_KITS')") class TouchUpKitController( private val touchUpKitService: TouchUpKitService diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt index 5d6bd11..bab0b70 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt @@ -8,6 +8,7 @@ import dev.fyloz.colorrecipesexplorer.model.validation.or import dev.fyloz.colorrecipesexplorer.repository.UserRepository import dev.fyloz.colorrecipesexplorer.repository.GroupRepository import org.springframework.context.annotation.Lazy +import org.springframework.context.annotation.Profile import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException @@ -74,6 +75,7 @@ interface CreUserDetailsService : UserDetailsService { } @Service +@Profile("!emergency") class UserServiceImpl( userRepository: UserRepository, @Lazy val groupService: GroupService, @@ -229,6 +231,7 @@ class UserServiceImpl( const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans @Service +@Profile("!emergency") class GroupServiceImpl( private val userService: UserService, groupRepository: GroupRepository @@ -298,6 +301,7 @@ class GroupServiceImpl( } @Service +@Profile("!emergency") class CreUserDetailsServiceImpl( private val userService: UserService ) : diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt index 72a2f47..3e8e0a9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt @@ -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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationService.kt new file mode 100644 index 0000000..218946a --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationService.kt @@ -0,0 +1,155 @@ +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.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 + + /** + * 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 + + /** + * 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) + + /** + * 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" + +@Service +class ConfigurationServiceImpl( + @Lazy private val repository: ConfigurationRepository, + private val fileService: FileService, + private val fileConfiguration: FileConfiguration, + private val creProperties: CreProperties, + private val databaseProperties: DatabaseUpdaterProperties +) : ConfigurationService { + override fun getAll() = + ConfigurationType.values().mapNotNull { + try { + get(it) + } catch (_: ConfigurationNotSetException) { + null + } + } + + override fun getAll(formattedKeyList: String) = + formattedKeyList.split(';').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) { + 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 -> getAppVersion() + 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() +) + +const val APP_VERSION_PATH = "buildversion" + +fun getAppVersion(): Int = with(File(APP_VERSION_PATH)) { + this.readLines()[0].toInt() +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt index 4fb4ee3..77f10cb 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt @@ -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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt index cb5c3ab..fdf1bde 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -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( 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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt index e088ee4..8d0ce96 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt @@ -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( repository diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt index 92339e8..e977852 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt @@ -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 } @Service +@Profile("!emergency") class MixMaterialServiceImpl( mixMaterialRepository: MixMaterialRepository, @Lazy val materialService: MaterialService diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt index af2217c..72c0009 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt @@ -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 { @@ -26,6 +27,7 @@ interface MixTypeService : NamedModelService { } @Service +@Profile("!emergency") class MixTypeServiceImpl( mixTypeRepository: MixTypeRepository, @Lazy val materialService: MaterialService, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index c28ef6e..ee26293 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -7,6 +7,7 @@ import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository import dev.fyloz.colorrecipesexplorer.service.files.FileService import dev.fyloz.colorrecipesexplorer.utils.setAll import org.springframework.context.annotation.Lazy +import org.springframework.context.annotation.Profile import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile import java.io.File @@ -37,13 +38,15 @@ interface RecipeService : } @Service +@Profile("!emergency") class RecipeServiceImpl( recipeRepository: RecipeRepository, val companyService: CompanyService, val mixService: MixService, val recipeStepService: RecipeStepService, @Lazy val groupService: GroupService, - val recipeImageService: RecipeImageService + val recipeImageService: RecipeImageService, + val configService: ConfigurationService ) : AbstractExternalModelService( recipeRepository @@ -69,7 +72,7 @@ class RecipeServiceImpl( }.toSet(), this.groupsInformation, recipeImageService.getAllImages(this) - .map { this.imageUrl(it) } + .map { this.imageUrl(configService.get(ConfigurationType.INSTANCE_URL).content, it) } .toSet() ) @@ -206,6 +209,7 @@ const val RECIPE_IMAGE_ID_DELIMITER = "_" const val RECIPE_IMAGE_EXTENSION = ".jpg" @Service +@Profile("!emergency") class RecipeImageServiceImpl( val fileService: FileService ) : RecipeImageService { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt index 2475ed3..0bde45d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt @@ -6,6 +6,7 @@ import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository import dev.fyloz.colorrecipesexplorer.utils.findDuplicated import dev.fyloz.colorrecipesexplorer.utils.hasGaps +import org.springframework.context.annotation.Profile import org.springframework.http.HttpStatus import org.springframework.stereotype.Service @@ -22,6 +23,7 @@ interface RecipeStepService : ModelService { } @Service +@Profile("!emergency") class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) : AbstractModelService(recipeStepRepository), RecipeStepService { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/touchupkit/TouchUpKitService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/touchupkit/TouchUpKitService.kt index 79c9440..e17ba65 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/touchupkit/TouchUpKitService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/touchupkit/TouchUpKitService.kt @@ -1,13 +1,16 @@ package dev.fyloz.colorrecipesexplorer.service.touchupkit import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties +import dev.fyloz.colorrecipesexplorer.model.ConfigurationType import dev.fyloz.colorrecipesexplorer.model.touchupkit.* import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository import dev.fyloz.colorrecipesexplorer.rest.TOUCH_UP_KIT_CONTROLLER_PATH import dev.fyloz.colorrecipesexplorer.service.AbstractExternalModelService +import dev.fyloz.colorrecipesexplorer.service.ConfigurationService import dev.fyloz.colorrecipesexplorer.service.ExternalModelService import dev.fyloz.colorrecipesexplorer.service.files.FileService import dev.fyloz.colorrecipesexplorer.utils.* +import org.springframework.context.annotation.Profile import org.springframework.core.io.ByteArrayResource import org.springframework.stereotype.Service @@ -34,10 +37,11 @@ interface TouchUpKitService : } @Service +@Profile("!emergency") class TouchUpKitServiceImpl( private val fileService: FileService, - touchUpKitRepository: TouchUpKitRepository, - private val creProperties: CreProperties, + private val configService: ConfigurationService, + touchUpKitRepository: TouchUpKitRepository ) : AbstractExternalModelService( touchUpKitRepository ), TouchUpKitService { @@ -98,7 +102,7 @@ class TouchUpKitServiceImpl( } override fun generateJobPdfResource(job: String): ByteArrayResource { - if (creProperties.cacheGeneratedFiles) { + if (cacheGeneratedFiles()) { with(job.pdfDocumentPath()) { if (fileService.exists(this)) { return fileService.read(this) @@ -112,7 +116,7 @@ class TouchUpKitServiceImpl( } override fun String.cachePdfDocument(document: PdfDocument) { - if (!creProperties.cacheGeneratedFiles) return + if (!cacheGeneratedFiles()) return fileService.write(document.toByteArrayResource(), this.pdfDocumentPath(), true) } @@ -121,5 +125,8 @@ class TouchUpKitServiceImpl( "$TOUCH_UP_KIT_FILES_PATH/$this.pdf" private fun TouchUpKit.pdfUrl() = - "${creProperties.deploymentUrl}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project" + "${configService.get(ConfigurationType.INSTANCE_URL)}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project" + + private fun cacheGeneratedFiles() = + configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF).content == "true" } diff --git a/src/main/resources/application-emergency.properties b/src/main/resources/application-emergency.properties new file mode 100644 index 0000000..b35406d --- /dev/null +++ b/src/main/resources/application-emergency.properties @@ -0,0 +1 @@ +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration diff --git a/src/main/resources/application-mysql.properties b/src/main/resources/application-mysql.properties index 06d0241..c1fcf1c 100644 --- a/src/main/resources/application-mysql.properties +++ b/src/main/resources/application-mysql.properties @@ -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 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d6fad50..69a48e9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,9 +17,6 @@ entities.material-types.systemTypes[1].name=Base entities.material-types.systemTypes[1].prefix=BAS entities.material-types.systemTypes[1].usepercentages=false entities.material-types.baseName=Base -# Database manager -databaseupdater.username=root -databaseupdater.password=pass # DEBUG spring.jpa.show-sql=false # Do not modify @@ -33,3 +30,5 @@ spring.h2.console.enabled=false spring.jackson.deserialization.fail-on-null-for-primitives=true spring.jackson.default-property-inclusion=non_null spring.profiles.active=@spring.profiles.active@ + +spring.datasource.continue-on-error=true From feabcc5b76c0ceb7ad642bd12f7f497cf0f63d21 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Fri, 28 May 2021 18:18:01 -0400 Subject: [PATCH 8/9] Ajout des tests des configurations --- .gitlab-ci.yml | 4 +- build.gradle.kts | 4 + buildversion | 1 - .../DatabaseVersioning.kt | 3 +- .../model/Configuration.kt | 3 + .../service/ConfigurationService.kt | 43 ++- .../service/ConfigurationServiceTest.kt | 255 ++++++++++++++++++ .../service/MaterialServiceTest.kt | 2 +- .../service/RecipeServiceTest.kt | 2 +- .../service/files/TouchUpKitServiceTest.kt | 2 +- 10 files changed, 289 insertions(+), 30 deletions(-) delete mode 100644 buildversion create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 06825b8..3d4493e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -48,7 +48,7 @@ package: ARTIFACT_NAME: "ColorRecipesExplorer-backend-$CI_PIPELINE_IID" script: - docker rm $PACKAGE_CONTAINER_NAME || true - - docker run --name $PACKAGE_CONTAINER_NAME $CI_REGISTRY_IMAGE_GRADLE gradle bootJar + - docker run --name $PACKAGE_CONTAINER_NAME $CI_REGISTRY_IMAGE_GRADLE gradle bootJar -Pversion=$CI_PIPELINE_IID - docker cp $PACKAGE_CONTAINER_NAME:/usr/src/cre/build/libs/ColorRecipesExplorer.jar $ARTIFACT_NAME.jar - docker build -t $CI_REGISTRY_IMAGE_BACKEND --build-arg JDK_VERSION=$JDK_VERSION --build-arg PORT=$PORT --build-arg ARTIFACT_NAME=$ARTIFACT_NAME . - docker push $CI_REGISTRY_IMAGE_BACKEND @@ -81,4 +81,4 @@ deploy: script: - ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker stop $DEPLOYED_CONTAINER_NAME || true && docker rm $DEPLOYED_CONTAINER_NAME || true" - ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY && docker pull $CI_REGISTRY_IMAGE_BACKEND" - - ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker run -d -p $PORT:$PORT --name=$DEPLOYED_CONTAINER_NAME -v $DATA_VOLUME:/usr/bin/cre/data -e spring_profiles_active=$SPRING_PROFILES -e spring_datasource_username=$DB_USERNAME -e spring_datasource_password=$DB_PASSWORD -e spring_datasource_url=$DB_URL -e 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" diff --git a/build.gradle.kts b/build.gradle.kts index bd9f8dd..efcc6f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,6 +62,10 @@ dependencies { implementation("org.springframework.cloud:spring-cloud-starter:2.2.8.RELEASE") } +springBoot { + buildInfo() +} + java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/buildversion b/buildversion deleted file mode 100644 index 3a2e3f4..0000000 --- a/buildversion +++ /dev/null @@ -1 +0,0 @@ --1 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt index 9b0411a..ca54fa3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/DatabaseVersioning.kt @@ -14,6 +14,7 @@ import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.DependsOn import org.springframework.context.annotation.Profile import org.springframework.core.env.ConfigurableEnvironment +import java.lang.RuntimeException import javax.sql.DataSource const val SUPPORTED_DATABASE_VERSION = 5 @@ -44,7 +45,7 @@ class DataSourceConfiguration { username = databaseUsername password = databasePassword }) - } catch (ex: CreDatabaseException) { + } catch (ex: Exception) { logger.error("Could not access database, restarting in emergency mode...", ex) emergencyMode = true diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt index e98a938..8f971ba 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt @@ -39,6 +39,9 @@ data class ConfigurationEntity( ) { fun toConfiguration() = configuration(key.toConfigurationType(), content, lastUpdated) + + override fun equals(other: Any?) = + other is ConfigurationEntity && key == other.key && content == other.content } data class ConfigurationDto( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationService.kt index 218946a..d18384d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationService.kt @@ -8,6 +8,7 @@ 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 @@ -51,6 +52,7 @@ interface ConfigurationService { 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( @@ -58,7 +60,8 @@ class ConfigurationServiceImpl( private val fileService: FileService, private val fileConfiguration: FileConfiguration, private val creProperties: CreProperties, - private val databaseProperties: DatabaseUpdaterProperties + private val databaseProperties: DatabaseUpdaterProperties, + private val buildInfo: BuildProperties ) : ConfigurationService { override fun getAll() = ConfigurationType.values().mapNotNull { @@ -70,7 +73,7 @@ class ConfigurationServiceImpl( } override fun getAll(formattedKeyList: String) = - formattedKeyList.split(';').map(this::get) + formattedKeyList.split(CONFIGURATION_FORMATTED_LIST_DELIMITER).map(this::get) override fun get(key: String) = get(key.toConfigurationType()) @@ -131,25 +134,19 @@ class ConfigurationServiceImpl( ConfigurationType.TOUCH_UP_KIT_CACHE_PDF -> "true" else -> "" } -} - -private fun getComputedConfiguration(key: ConfigurationType) = configuration( - key, when (key) { - ConfigurationType.EMERGENCY_MODE_ENABLED -> emergencyMode - ConfigurationType.VERSION -> getAppVersion() - 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() -) - -const val APP_VERSION_PATH = "buildversion" - -fun getAppVersion(): Int = with(File(APP_VERSION_PATH)) { - this.readLines()[0].toInt() + + 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() + ) } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt new file mode 100644 index 0000000..5275e97 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/ConfigurationServiceTest.kt @@ -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() + private val fileConfiguration = mockk() + 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()) } 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 { it in unsetConfigurationTypes }) } answers { + throw ConfigurationNotSetException(this.firstArg() as ConfigurationType) + } + every { service.get(match { it !in unsetConfigurationTypes }) } answers { + val type = firstArg() + 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()) } answers { + val key = firstArg() + 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() + 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 { 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 { service.set(type, content) }) { + assertEquals(type, this.type) + } + + verify { + service.set(type, content) + } + confirmVerified(service) + } +} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt index 9b3dacb..dd42bf2 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt @@ -23,7 +23,7 @@ class MaterialServiceTest : private val materialTypeService: MaterialTypeService = mock() private val fileService: FileService = mock() override val service: MaterialService = - spy(MaterialServiceImpl(repository, recipeService, mixService, materialTypeService, fileService)) + spy(MaterialServiceImpl(repository, recipeService, mixService, materialTypeService, fileService, mock())) override val entity: Material = material(id = 0L, name = "material") private val entityOutput = materialOutputDto(entity) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt index a8ccd0b..8d4e612 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt @@ -27,7 +27,7 @@ class RecipeServiceTest : private val groupService: GroupService = mock() private val recipeStepService: RecipeStepService = mock() override val service: RecipeService = - spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService, mock())) + spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService, mock(), mock())) private val company: Company = company(id = 0L) override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt index 3813fd5..227cbf8 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt @@ -21,7 +21,7 @@ private class TouchUpKitServiceTestContext { val creProperties = mockk { every { cacheGeneratedFiles } returns false } - val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, touchUpKitRepository, creProperties)) + val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, mockk(), touchUpKitRepository)) val pdfDocumentData = mockk() val pdfDocument = mockk { mockkStatic(PdfDocument::toByteArrayResource) From e0a472f1a2146b5f007378d80e10ffd9984b6130 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Fri, 28 May 2021 19:21:23 -0400 Subject: [PATCH 9/9] Correction des tests --- .../dev/fyloz/colorrecipesexplorer/model/Configuration.kt | 6 ++++++ .../service/files/TouchUpKitServiceTest.kt | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt index 8f971ba..9cb732c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Configuration.kt @@ -42,6 +42,12 @@ data class ConfigurationEntity( 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( diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt index 227cbf8..cdbebf7 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/TouchUpKitServiceTest.kt @@ -1,7 +1,10 @@ package dev.fyloz.colorrecipesexplorer.service.files import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties +import dev.fyloz.colorrecipesexplorer.model.ConfigurationType +import dev.fyloz.colorrecipesexplorer.model.configuration import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository +import dev.fyloz.colorrecipesexplorer.service.ConfigurationService import dev.fyloz.colorrecipesexplorer.service.touchupkit.TOUCH_UP_TEXT_EN import dev.fyloz.colorrecipesexplorer.service.touchupkit.TOUCH_UP_TEXT_FR import dev.fyloz.colorrecipesexplorer.service.touchupkit.TouchUpKitServiceImpl @@ -21,7 +24,8 @@ private class TouchUpKitServiceTestContext { val creProperties = mockk { every { cacheGeneratedFiles } returns false } - val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, mockk(), touchUpKitRepository)) + val configService = mockk(relaxed = true) + val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, configService, touchUpKitRepository)) val pdfDocumentData = mockk() val pdfDocument = mockk { mockkStatic(PdfDocument::toByteArrayResource) @@ -81,6 +85,7 @@ class TouchUpKitServiceTest { every { creProperties.cacheGeneratedFiles } returns true every { fileService.exists(any()) } returns true every { fileService.read(any()) } returns pdfDocumentData + every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF, "true") val redResource = touchUpKitService.generateJobPdfResource(job) @@ -109,6 +114,7 @@ class TouchUpKitServiceTest { fun `cachePdfDocument() writes the given document to the FileService when cache is enabled`() { test { every { creProperties.cacheGeneratedFiles } returns true + every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF, "true") with(touchUpKitService) { job.cachePdfDocument(pdfDocument)