Ajout des comptes pour l'API REST.

This commit is contained in:
FyloZ 2020-10-15 23:38:13 -04:00
parent a16844d747
commit 3bafc3d9ef
39 changed files with 1082 additions and 155 deletions

View File

@ -9,6 +9,8 @@ plugins {
id("com.leobia.gradle.sassjavacompiler") version "0.2.1"
id("io.freefair.lombok") version "5.2.1"
id("org.springframework.boot") version "2.3.4.RELEASE"
id("org.jetbrains.kotlin.plugin.spring") version "1.4.10"
id("org.jetbrains.kotlin.plugin.jpa") version "1.4.10"
}
repositories {
@ -24,23 +26,29 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
implementation("org.springframework.boot:spring-boot-starter-data-jpa:2.1.4.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-jdbc:2.1.4.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf:2.1.4.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-web:2.1.4.RELEASE")
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-thymeleaf: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")
testImplementation("org.springframework.boot:spring-boot-starter-test:2.3.4.RELEASE")
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:2.3.4.RELEASE")
implementation("org.springframework.boot:spring-boot-devtools:2.3.4.RELEASE")
implementation("javax.xml.bind:jaxb-api:2.3.0")
implementation("io.jsonwebtoken:jjwt:0.9.1")
implementation("org.apache.poi:poi-ooxml:4.1.0")
implementation("org.apache.pdfbox:pdfbox:2.0.4")
implementation("org.springframework.boot:spring-boot-configuration-processor:2.1.4.RELEASE")
implementation("org.springframework.boot:spring-boot-devtools:2.1.4.RELEASE")
implementation("com.atlassian.commonmark:commonmark:0.13.1")
implementation("commons-io:commons-io:2.6")
implementation("org.springframework:spring-test:5.1.6.RELEASE")
implementation("org.springframework.boot:spring-boot-test-autoconfigure:2.1.4.RELEASE")
implementation("org.mockito:mockito-core:2.23.4")
implementation("org.junit.jupiter:junit-jupiter-api:5.3.2")
runtimeOnly("com.h2database:h2:1.4.199")
testImplementation("org.springframework.boot:spring-boot-starter-test:2.1.6.RELEASE")
compileOnly("org.projectlombok:lombok:1.18.10")
}

View File

@ -5,10 +5,12 @@ import dev.fyloz.trial.colorrecipesexplorer.core.model.config.MaterialTypeProper
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching
@SpringBootApplication
@EnableConfigurationProperties(MaterialTypeProperties::class, CREProperties::class)
open class ColorRecipesExplorerApplication
@EnableCaching
class ColorRecipesExplorerApplication
fun main(args: Array<String>) {
runApplication<ColorRecipesExplorerApplication>(*args)

View File

@ -46,13 +46,18 @@ public class SpringConfiguration {
this.materialTypeProperties = materialTypeProperties;
}
@Bean
public Logger getLogger() {
return LoggerFactory.getLogger(ColorRecipesExplorerApplication.class);
}
@Bean
public void setPreferences() {
Preferences.urlUsePort = creProperties.isUrlUsePort();
Preferences.urlUseHttps = creProperties.isUrlUseHttps();
Preferences.uploadDirectory = creProperties.getUploadDirectory();
Preferences.passwordsFileName = creProperties.getPasswordFile();
Preferences.logger = LoggerFactory.getLogger(ColorRecipesExplorerApplication.class);
Preferences.logger = getLogger();
Preferences.messageSource = messageSource;
Preferences.baseMaterialTypeName = materialTypeProperties.getBaseName();
}

View File

@ -0,0 +1,215 @@
package dev.fyloz.trial.colorrecipesexplorer.core.configuration
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.fyloz.trial.colorrecipesexplorer.core.model.Employee
import dev.fyloz.trial.colorrecipesexplorer.core.model.EmployeeLoginRequest
import dev.fyloz.trial.colorrecipesexplorer.core.model.EmployeePermission
import dev.fyloz.trial.colorrecipesexplorer.core.services.EmployeeService
import dev.fyloz.trial.colorrecipesexplorer.core.services.EmployeeUserDetailsService
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import org.slf4j.Logger
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer
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.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import org.springframework.stereotype.Component
import org.springframework.util.Assert
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import java.util.*
import javax.annotation.PostConstruct
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableConfigurationProperties(SecurityConfigurationProperties::class)
class WebSecurityConfig(
val restAuthenticationEntryPoint: RestAuthenticationEntryPoint,
val securityConfigurationProperties: SecurityConfigurationProperties,
val logger: Logger) : WebSecurityConfigurerAdapter() {
@Autowired
private lateinit var userDetailsService: EmployeeUserDetailsService
@Autowired
private lateinit var employeeService: EmployeeService
override fun configure(authBuilder: AuthenticationManagerBuilder) {
authBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder())
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
@Bean
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
return UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", CorsConfiguration().applyPermitDefaultValues())
}
}
@PostConstruct
fun createRootUser() {
val rootUserCredentials = securityConfigurationProperties.root
Assert.notNull(rootUserCredentials, "No root user has been defined.")
Assert.notNull(rootUserCredentials!!.id, "The root user has no identifier defined.")
Assert.notNull(rootUserCredentials.password, "The root user has no password defined.")
if (!employeeService.existsById(rootUserCredentials.id!!)) {
employeeService.save(Employee(rootUserCredentials.id!!, "Root", "Employee", passwordEncoder().encode(rootUserCredentials.password!!), true, permissions = mutableListOf(EmployeePermission.ADMIN)))
}
}
override fun configure(http: HttpSecurity) {
fun ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry.generateAuthorizations():
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry {
ControllerAuthorizations.values().forEach { controller ->
val antMatcher = controller.antMatcher
controller.permissions.forEach {
antMatchers(it.key, antMatcher).hasAuthority(it.value.name)
logger.debug("Added authorization for path '$antMatcher': ${it.key.name} -> ${it.value.name}")
}
}
return this
}
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/").permitAll()
.antMatchers("/api/login").permitAll()
.antMatchers(HttpMethod.GET, "/api/employee/current").authenticated()
.generateAuthorizations()
.and()
.addFilter(JwtAuthenticationFilter(authenticationManager(), employeeService, securityConfigurationProperties))
.addFilter(JwtAuthorizationFilter(userDetailsService, securityConfigurationProperties, authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
}
@Component
class RestAuthenticationEntryPoint : AuthenticationEntryPoint {
override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
}
class JwtAuthenticationFilter(
val authManager: AuthenticationManager,
val employeeService: EmployeeService,
val securityConfigurationProperties: SecurityConfigurationProperties
) : UsernamePasswordAuthenticationFilter() {
init {
setFilterProcessesUrl("/api/login")
}
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, EmployeeLoginRequest::class.java)
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
}
override fun successfulAuthentication(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain, authResult: Authentication) {
val jwtSecret = securityConfigurationProperties.jwtSecret
val jwtDuration = securityConfigurationProperties.jwtDuration
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
Assert.notNull(jwtDuration, "No JWT duration has been defined.")
val employeeId = (authResult.principal as User).username
employeeService.updateLastLoginTime(employeeId.toLong())
val token = Jwts.builder()
.setSubject(employeeId)
.setExpiration(Date(System.currentTimeMillis() + jwtDuration!!))
.signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
.compact()
response.addHeader("Authorization", "Bearer $token")
}
}
class JwtAuthorizationFilter(
val userDetailsService: EmployeeUserDetailsService,
val securityConfigurationProperties: SecurityConfigurationProperties,
authenticationManager: AuthenticationManager
) : BasicAuthenticationFilter(authenticationManager) {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
val header = request.getHeader("Authorization")
if (header != null && header.startsWith("Bearer")) {
val authenticationToken = getAuthentication(request)
SecurityContextHolder.getContext().authentication = authenticationToken
}
chain.doFilter(request, response)
}
private fun getAuthentication(request: HttpServletRequest): UsernamePasswordAuthenticationToken? {
val jwtSecret = securityConfigurationProperties.jwtSecret
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
val token = request.getHeader("Authorization")
if (token != null) {
val employeeId = Jwts.parser()
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
return if (employeeId != null) {
val employeeDetails = userDetailsService.loadUserByUsername(employeeId)
UsernamePasswordAuthenticationToken(employeeDetails.username, null, employeeDetails.authorities)
} else null
}
return null
}
}
@ConfigurationProperties("cre.security")
class SecurityConfigurationProperties {
var jwtSecret: String? = null
var jwtDuration: Long? = null
var root: RootUserCredentials? = null
class RootUserCredentials(var id: Long? = null, var password: String? = null)
}
private enum class ControllerAuthorizations(
val antMatcher: String,
val permissions: Map<HttpMethod, EmployeePermission>
) {
EMPLOYEE_GROUP("/api/employee/group/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE_GROUP,
HttpMethod.POST to EmployeePermission.EDIT_EMPLOYEE_GROUP,
HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE_GROUP,
HttpMethod.DELETE to EmployeePermission.REMOVE_EMPLOYEE_GROUP
)),
EMPLOYEE("/api/employee/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE,
HttpMethod.POST to EmployeePermission.EDIT_EMPLOYEE,
HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE,
HttpMethod.DELETE to EmployeePermission.REMOVE_EMPLOYEE
))
}

View File

@ -0,0 +1,55 @@
package dev.fyloz.trial.colorrecipesexplorer.core.exception
import com.fasterxml.jackson.annotation.JsonProperty
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.*
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.validation.FieldError
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
abstract class RestException(val httpStatus: HttpStatus) : RuntimeException() {
abstract val exceptionMessage: String
abstract fun buildBody(): RestExceptionBody
override val message: String by lazy { exceptionMessage }
open inner class RestExceptionBody(val status: Int = httpStatus.value(), @JsonProperty("message") val message: String = exceptionMessage)
}
@ControllerAdvice
@Profile("rest")
class RestResponseEntityExceptionHandler : ResponseEntityExceptionHandler() {
@ExceptionHandler(ModelException::class)
fun handleModelExceptions(exception: ModelException, request: WebRequest): ResponseEntity<Any> {
return handleRestExceptions(exception.toRestException(), request)
}
@ExceptionHandler(RestException::class)
fun handleRestExceptions(exception: RestException, request: WebRequest): ResponseEntity<Any> {
return handleExceptionInternal(exception, exception.buildBody(), HttpHeaders(), exception.httpStatus, request)
}
override fun handleMethodArgumentNotValid(ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatus, request: WebRequest): ResponseEntity<Any> {
val errors = hashMapOf<String, String>()
ex.bindingResult.allErrors.forEach {
val fieldName = (it as FieldError).field
val errorMessage = it.defaultMessage
errors[fieldName] = errorMessage
}
return ResponseEntity(errors, headers, status)
}
}
fun ModelException.toRestException(): RestException {
return when (this) {
is EntityAlreadyExistsException -> EntityAlreadyExistsRestException(requestedId)
is EntityNotFoundException -> EntityNotFoundRestException(requestedId)
else -> throw UnsupportedOperationException("Cannot convert ${this::class.simpleName} to REST exception")
}
}

View File

@ -1,34 +1,37 @@
package dev.fyloz.trial.colorrecipesexplorer.core.exception.model;
import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel;
import lombok.Getter;
import lombok.NonNull;
@Getter
public class EntityAlreadyExistsException extends ModelException {
@NonNull
private IdentifierType identifierType;
private String identifierName;
@NonNull
private Object requestedId;
public EntityAlreadyExistsException(EntityAlreadyExistsException ex) {
this(ex.type, ex.identifierType, ex.identifierName, ex.requestedId);
}
public EntityAlreadyExistsException(Class<? extends IModel> type, IdentifierType identifierType, Object requestedId) {
super(type);
this.identifierType = identifierType;
this.requestedId = requestedId;
}
public EntityAlreadyExistsException(Class<? extends IModel> type, IdentifierType identifierType, String identifierName, Object requestedId) {
super(type);
this.identifierType = identifierType;
this.identifierName = identifierName != null ? identifierName : identifierType.getName();
this.requestedId = requestedId;
}
}
//package dev.fyloz.trial.colorrecipesexplorer.core.exception.model;
//
//import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel;
//import lombok.Getter;
//import lombok.NonNull;
//import org.springframework.http.HttpStatus;
//import org.springframework.web.bind.annotation.ResponseStatus;
//
//@Getter
//@ResponseStatus(HttpStatus.CONFLICT)
//public class EntityAlreadyExistsException extends ModelException {
//
// @NonNull
// private IdentifierType identifierType;
//
// private String identifierName;
//
// @NonNull
// private Object requestedId;
//
// public EntityAlreadyExistsException(EntityAlreadyExistsException ex) {
// this(ex.type, ex.identifierType, ex.identifierName, ex.requestedId);
// }
//
// public EntityAlreadyExistsException(Class<? extends IModel> type, IdentifierType identifierType, Object requestedId) {
// super(type);
// this.identifierType = identifierType;
// this.requestedId = requestedId;
// }
//
// public EntityAlreadyExistsException(Class<? extends IModel> type, IdentifierType identifierType, String identifierName, Object requestedId) {
// super(type);
// this.identifierType = identifierType;
// this.identifierName = identifierName != null ? identifierName : identifierType.getName();
// this.requestedId = requestedId;
// }
//}

View File

@ -0,0 +1,20 @@
package dev.fyloz.trial.colorrecipesexplorer.core.exception.model
import dev.fyloz.trial.colorrecipesexplorer.core.exception.RestException
import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus
class EntityAlreadyExistsException(modelType: Class<out IModel>, val identifierType: IdentifierType, val identifierName: String?, val requestedId: Any) : ModelException(modelType) {
constructor(modelType: Class<out IModel>, identifierType: IdentifierType, requestedId: Any) : this(modelType, identifierType, identifierType.name, requestedId)
constructor(exception: EntityAlreadyExistsException) : this(exception.type, exception.identifierType, exception.identifierName, exception.requestedId)
}
@ResponseStatus(HttpStatus.CONFLICT)
class EntityAlreadyExistsRestException(val value: Any) : RestException(HttpStatus.CONFLICT) {
override val exceptionMessage: String = "An entity with the given identifier already exists"
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {
val id = value
}
}

View File

@ -1,39 +1,39 @@
package dev.fyloz.trial.colorrecipesexplorer.core.exception.model;
import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel;
import lombok.Getter;
/**
* Représente une exception qui sera lancée lorsqu'un objet du modèle n'est pas trouvé.
*/
@Getter
public class EntityNotFoundException extends ModelException {
/**
* Le type d'identifiant utilisé
*/
private IdentifierType identifierType;
/**
* Le nom de l'identifiant utilisé (optionnel)
*/
private String identifierName;
/**
* La valeur de l'identifiant
*/
private Object requestedId;
public EntityNotFoundException(Class<? extends IModel> type, IdentifierType identifierType, Object requestedId) {
super(type);
this.identifierType = identifierType;
this.requestedId = requestedId;
}
public EntityNotFoundException(Class<? extends IModel> type, IdentifierType identifierType, String identifierName, Object requestedId) {
super(type);
this.identifierType = identifierType;
this.identifierName = identifierName != null ? identifierName : identifierType.getName();
this.requestedId = requestedId;
}
}
//package dev.fyloz.trial.colorrecipesexplorer.core.exception.model;
//
//import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel;
//import lombok.Getter;
//
///**
// * Représente une exception qui sera lancée lorsqu'un objet du modèle n'est pas trouvé.
// */
//@Getter
//public class EntityNotFoundException extends ModelException {
//
// /**
// * Le type d'identifiant utilisé
// */
// private IdentifierType identifierType;
//
// /**
// * Le nom de l'identifiant utilisé (optionnel)
// */
// private String identifierName;
//
// /**
// * La valeur de l'identifiant
// */
// private Object requestedId;
//
// public EntityNotFoundException(Class<? extends IModel> type, IdentifierType identifierType, Object requestedId) {
// super(type);
// this.identifierType = identifierType;
// this.requestedId = requestedId;
// }
//
// public EntityNotFoundException(Class<? extends IModel> type, IdentifierType identifierType, String identifierName, Object requestedId) {
// super(type);
// this.identifierType = identifierType;
// this.identifierName = identifierName != null ? identifierName : identifierType.getName();
// this.requestedId = requestedId;
// }
//}

View File

@ -0,0 +1,19 @@
package dev.fyloz.trial.colorrecipesexplorer.core.exception.model
import dev.fyloz.trial.colorrecipesexplorer.core.exception.RestException
import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus
class EntityNotFoundException(modelType: Class<out IModel>, val identifierType: IdentifierType, val identifierName: String, val requestedId: Any) : ModelException(modelType) {
constructor(modelType: Class<out IModel>, identifierType: IdentifierType, requestedId: Any) : this(modelType, identifierType, identifierType.name, requestedId)
}
@ResponseStatus(HttpStatus.NOT_FOUND)
class EntityNotFoundRestException(val value: Any) : RestException(HttpStatus.NOT_FOUND) {
override val exceptionMessage: String = "An entity could not be found with the given identifier"
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {
val id = value
}
}

View File

@ -8,7 +8,7 @@ import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel;
public class ModelException extends RuntimeException {
/**
* Le type de modèle qui est sujette à l'exception
* Le type de modèle qui est sujet à l'exception
*/
protected Class<? extends IModel> type;

View File

@ -0,0 +1,172 @@
package dev.fyloz.trial.colorrecipesexplorer.core.model
import com.fasterxml.jackson.annotation.JsonIgnore
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
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
class Employee(
@Id
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
override val id: Long,
val firstName: String = "",
val lastName: String = "",
@JsonIgnore
val password: String = "",
@JsonIgnore
val isRoot: Boolean = false,
@field:ManyToOne
@Fetch(FetchMode.SELECT)
var group: EmployeeGroup? = null,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
val permissions: MutableList<EmployeePermission> = mutableListOf(),
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@Fetch(FetchMode.SUBSELECT)
val excludedPermissions: MutableList<EmployeePermission> = mutableListOf(),
val lastLoginTime: LocalDateTime? = null
) : IModel
/** DTO for creating employees. The [Employee] entity doesn't allow to modify passwords. */
data class EmployeeDto(
@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,
@field:ManyToOne
@Fetch(FetchMode.SELECT)
var group: EmployeeGroup? = null,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
val permissions: MutableList<EmployeePermission> = mutableListOf(),
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@Fetch(FetchMode.SUBSELECT)
val excludedPermissions: MutableList<EmployeePermission> = mutableListOf()
)
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
data class EmployeeGroup(
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
override val id: Long? = null,
@Column(unique = true)
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String = "",
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableList<EmployeePermission> = mutableListOf(),
@OneToMany
@JsonIgnore
val employees: MutableList<Employee> = mutableListOf()
) : IModel
data class EmployeeLoginRequest(val id: Long, val password: String)
enum class EmployeePermission(val impliedPermissions: List<EmployeePermission> = listOf()) {
// View
VIEW_EMPLOYEE,
VIEW_EMPLOYEE_GROUP,
VIEW(listOf(
)),
// Edit
EDIT_EMPLOYEE,
EDIT_EMPLOYEE_GROUP,
EDIT(listOf(
)),
// Remove
REMOVE_EMPLOYEE,
REMOVE_EMPLOYEE_GROUP,
REMOVE(listOf(
)),
ADMIN(listOf(
VIEW,
EDIT,
REMOVE,
// Admin only permissions
VIEW_EMPLOYEE,
VIEW_EMPLOYEE_GROUP,
EDIT_EMPLOYEE,
EDIT_EMPLOYEE_GROUP,
REMOVE_EMPLOYEE,
REMOVE_EMPLOYEE_GROUP
));
operator fun contains(permission: EmployeePermission): Boolean {
return permission == this || impliedPermissions.any { permission in it }
}
}
/** Gets [GrantedAuthority]s of the given [Employee]. */
fun Employee.getAuthorities(): MutableCollection<GrantedAuthority> {
return getPermissions().map { it.toAuthority() }.toMutableSet()
}
/** Gets [EmployeePermission]s of the given [Employee]. */
fun Employee.getPermissions(): Iterable<EmployeePermission> {
val grantedPermissions: MutableSet<EmployeePermission> = mutableSetOf()
if (group != null) grantedPermissions.addAll(group!!.permissions.flatMap { it.flat() }.filter { excludedPermissions.isEmpty() || excludedPermissions.any { excludedPermission -> it !in excludedPermission } })
grantedPermissions.addAll(permissions.flatMap { it.flat() }.filter { excludedPermissions.isEmpty() || excludedPermissions.any { excludedPermission -> it !in excludedPermission } })
return grantedPermissions
}
private fun EmployeePermission.flat(): Iterable<EmployeePermission> {
return mutableSetOf(this).apply {
impliedPermissions.forEach {
addAll(it.flat())
}
}
}
/** Converts the given [EmployeePermission] to a [GrantedAuthority]. */
private fun EmployeePermission.toAuthority(): GrantedAuthority {
return SimpleGrantedAuthority(name)
}

View File

@ -10,7 +10,6 @@ import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.NullIdentifierE
import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel;
import org.slf4j.Logger;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.lang.NonNull;
import org.springframework.transaction.annotation.Transactional;
import javax.validation.constraints.NotNull;
@ -19,13 +18,13 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public abstract class AbstractService<E extends IModel, R extends JpaRepository<E, Long>> implements IGenericService<E> {
public abstract class AbstractJavaService<E extends IModel, R extends JpaRepository<E, Long>> implements IGenericJavaService<E> {
protected Logger logger = Preferences.logger;
protected R dao;
protected Class<E> type;
public AbstractService(Class<E> type) {
public AbstractJavaService(Class<E> type) {
this.type = type;
}
@ -70,7 +69,7 @@ public abstract class AbstractService<E extends IModel, R extends JpaRepository<
}
@Override
public E update(@NonNull E entity) {
public E update(@NotNull E entity) {
if (entity.getId() == null) throw new NullIdentifierException(type);
if (!existsById(entity.getId()))
throw new EntityNotFoundException(type, ModelException.IdentifierType.ID, entity.getId());
@ -79,7 +78,7 @@ public abstract class AbstractService<E extends IModel, R extends JpaRepository<
}
@Override
public void delete(@NonNull E entity) {
public void delete(@NotNull E entity) {
dao.delete(entity);
}
@ -111,7 +110,7 @@ public abstract class AbstractService<E extends IModel, R extends JpaRepository<
* @return Si l'entité est valide pour l'édition.
*/
@Deprecated(since = "1.3.0", forRemoval = true)
public boolean isValidForUpdate(@NonNull E entity) {
public boolean isValidForUpdate(@NotNull E entity) {
return entity.getId() != null && existsById(entity.getId());
}

View File

@ -0,0 +1,153 @@
package dev.fyloz.trial.colorrecipesexplorer.core.services
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.*
import dev.fyloz.trial.colorrecipesexplorer.core.model.*
import dev.fyloz.trial.colorrecipesexplorer.dao.EmployeeGroupRepository
import dev.fyloz.trial.colorrecipesexplorer.dao.EmployeeRepository
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
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import javax.transaction.Transactional
@Service
class EmployeeService(val employeeRepository: EmployeeRepository, val passwordEncoder: PasswordEncoder) :
AbstractModelService<Employee, EmployeeRepository>(employeeRepository, Employee::class.java) {
/** Check if an [Employee] with the given [firstName] and [lastName] exists. */
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean {
return repository.existsByFirstNameAndLastName(firstName, lastName)
}
override fun getAll(): Collection<Employee> {
return super.getAll().filter { !it.isRoot }
}
override fun getById(id: Long): Employee {
return getById(id, true)
}
/** Gets the employee with the given [id]. */
fun getById(id: Long, ignoreRoot: Boolean): Employee {
return super.getById(id).apply {
if (ignoreRoot && isRoot) throw EntityNotFoundRestException(id)
}
}
/** Saves the given [employee]. The password contained in the DTO will be hashed in the created [Employee]. */
fun save(employee: EmployeeDto): Employee {
return save(with(employee) {
Employee(id, firstName, lastName, passwordEncoder.encode(password), false, group, permissions, excludedPermissions)
})
}
override fun save(entity: Employee): Employee {
if (existsByFirstNameAndLastName(entity.firstName, entity.lastName))
throw EntityAlreadyExistsException(type, ModelException.IdentifierType.NAME, "${entity.firstName} ${entity.lastName}")
return super.save(entity)
}
/** Updates the last login time of the employee with the given [employeeId]. */
fun updateLastLoginTime(employeeId: Long) {
update(Employee(id = employeeId, lastLoginTime = LocalDateTime.now()), false)
}
override fun update(entity: Employee): Employee {
return update(entity, true)
}
/** Updates de given [entity]. **/
fun update(entity: Employee, ignoreRoot: Boolean): Employee {
val persistedEmployee = getById(entity.id, ignoreRoot)
with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
if (this != null && id != entity.id)
throw EntityAlreadyExistsRestException("${entity.firstName} ${entity.lastName}")
}
return super.update(with(entity) {
Employee(
id,
if (firstName.isNotBlank()) firstName else persistedEmployee.firstName,
if (lastName.isNotBlank()) lastName else persistedEmployee.lastName,
persistedEmployee.password,
if (ignoreRoot) false else persistedEmployee.isRoot,
persistedEmployee.group,
if (permissions.isNotEmpty()) permissions else persistedEmployee.permissions,
if (excludedPermissions.isNotEmpty()) excludedPermissions else persistedEmployee.excludedPermissions,
lastLoginTime ?: persistedEmployee.lastLoginTime
)
})
}
/** Adds the given [permission] to the employee with the given [employeeId]. */
fun addPermission(employeeId: Long, permission: EmployeePermission) = super.update(getById(employeeId).apply { permissions += permission })
/** Removes the given [permission] from the employee with the given [employeeId]. */
fun removePermission(employeeId: Long, permission: EmployeePermission) = super.update(getById(employeeId).apply { permissions -= permission })
/** Adds the given [excludedPermission] to the employee with the given [employeeId]. */
fun addExcludedPermission(employeeId: Long, excludedPermission: EmployeePermission) = super.update(getById(employeeId).apply { excludedPermissions += excludedPermission })
/** Removes the given [excludedPermission] to the employee with the given [employeeId]. */
fun removeExcludedPermission(employeeId: Long, excludedPermission: EmployeePermission) = super.update(getById(employeeId).apply { excludedPermissions -= excludedPermission })
}
@Service
class EmployeeGroupService(val employeeGroupRepository: EmployeeGroupRepository, val employeeService: EmployeeService) : AbstractModelService<EmployeeGroup, EmployeeGroupRepository>(employeeGroupRepository, EmployeeGroup::class.java) {
/** Adds the employee with the given [employeeId] to the group with the given [groupId]. */
fun addEmployeeToGroup(groupId: Long, employeeId: Long) {
addEmployeeToGroup(getById(groupId), employeeService.getById(employeeId))
}
/**
* Adds a given [employee] to a given [group].
*
* If the [employee] is already in the [group], nothing will be done.
* If the [employee] is already in a group, it will be removed from it.
*/
@Transactional
fun addEmployeeToGroup(group: EmployeeGroup, employee: Employee) {
if (employee.group != null) removeEmployeeFromGroup(employee.group!!, employee)
if (employee.group == group) return
group.employees.add(employee)
employee.group = group
update(group)
employeeService.update(employee)
}
/** Removes the employee with the given [employeeId] from the group with the given [groupId]. */
fun removeEmployeeFromGroup(groupId: Long, employeeId: Long) =
removeEmployeeFromGroup(getById(groupId), employeeService.getById(employeeId))
/** Removes a given [employee] from the given [group]. */
@Transactional
fun removeEmployeeFromGroup(group: EmployeeGroup, employee: Employee) {
if (employee.group == null) return
group.employees.remove(employee)
employee.group = null
update(group)
employeeService.update(employee)
}
}
@Service
class EmployeeUserDetailsService(val employeeService: EmployeeService) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
try {
return loadUserByEmployeeId(username.toLong())
} catch (ex: EntityNotFoundException) {
throw UsernameNotFoundException(username)
}
}
/** Loads an [User] for the given [employeeId]. */
fun loadUserByEmployeeId(employeeId: Long): UserDetails {
val employee = employeeService.getById(employeeId, false)
return User(employee.id.toString(), employee.password, employee.getAuthorities())
}
}

View File

@ -5,7 +5,7 @@ import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel;
import java.util.Collection;
import java.util.List;
public interface IGenericService<T extends IModel> {
public interface IGenericJavaService<T extends IModel> {
@Deprecated(since = "1.3.0", forRemoval = true)
boolean exists(T entity);

View File

@ -0,0 +1,79 @@
package dev.fyloz.trial.colorrecipesexplorer.core.services
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.EntityNotFoundException
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.EntityNotFoundRestException
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.ModelException
import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.findByIdOrNull
/** A service implementing the basics CRUD operations. */
interface IGenericService<E> {
/** Gets all entities. */
fun getAll(): Collection<E>
/** Saves a given [entity]. */
fun save(entity: E): E
/** Saves all given [entities]. */
fun saveAll(entities: Iterable<E>): Collection<E>
/** Updates a given [entity]. */
fun update(entity: E): E
/** Deletes a given [entity]. */
fun delete(entity: E)
/** Deletes all give [entities]. */
fun deleteAll(entities: Iterable<E>)
}
/** A service for entities implementing the [IModel] interface. This service implements CRUD operations for [Long] identifiers. */
interface IGenericModelService<E : IModel> : IGenericService<E> {
/** Checks if an entity with the given [id] exists. */
fun existsById(id: Long): Boolean
/** Gets the entity with the given [id]. */
fun getById(id: Long): E
/** Deletes the entity with the given [id]. */
fun deleteById(id: Long)
}
abstract class AbstractService<E, R : JpaRepository<E, *>>(val repository: R, val type: Class<out IModel>) : IGenericService<E> {
override fun getAll(): Collection<E> = repository.findAll()
override fun save(entity: E): E = repository.save(entity)
override fun saveAll(entities: Iterable<E>): Collection<E> = entities.map(this::save)
override fun update(entity: E): E = repository.save(entity)
override fun delete(entity: E) = repository.delete(entity)
override fun deleteAll(entities: Iterable<E>) = entities.forEach(this::delete)
}
abstract class AbstractModelService<E : IModel, R : JpaRepository<E, Long>>(repository: R, type: Class<out IModel>) : AbstractService<E, R>(repository, type), IGenericModelService<E> {
override fun existsById(id: Long): Boolean = repository.existsById(id)
override fun getById(id: Long): E = repository.findByIdOrNull(id)
?: throw EntityNotFoundException(type, ModelException.IdentifierType.ID, id)
override fun save(entity: E): E {
with(entity.id) {
if (this != null && existsById(this))
throw EntityNotFoundRestException(this)
}
return super.save(entity)
}
override fun deleteById(id: Long) = delete(getById(id))
}
/** Transforms the given object to JSON. **/
fun Any.asJson(): String {
return jacksonObjectMapper().writeValueAsString(this)
}

View File

@ -4,8 +4,8 @@ import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.EntityAlreadyEx
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.EntityLinkedException;
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.ModelException;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Company;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractService;
import dev.fyloz.trial.colorrecipesexplorer.dao.CompanyDao;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractJavaService;
import dev.fyloz.trial.colorrecipesexplorer.dao.CompanyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -13,7 +13,7 @@ import org.springframework.stereotype.Service;
import javax.validation.constraints.NotNull;
@Service
public class CompanyService extends AbstractService<Company, CompanyDao> {
public class CompanyService extends AbstractJavaService<Company, CompanyRepository> {
private RecipeService recipeService;
@ -22,8 +22,8 @@ public class CompanyService extends AbstractService<Company, CompanyDao> {
}
@Autowired
public void setCompanyDao(CompanyDao companyDao) {
this.dao = companyDao;
public void setCompanyDao(CompanyRepository companyRepository) {
this.dao = companyRepository;
}
// Pour éviter les dépendances circulaires

View File

@ -5,9 +5,9 @@ import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.EntityNotFoundE
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.ModelException;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Material;
import dev.fyloz.trial.colorrecipesexplorer.core.model.MaterialType;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractService;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractJavaService;
import dev.fyloz.trial.colorrecipesexplorer.core.services.files.SimdutService;
import dev.fyloz.trial.colorrecipesexplorer.dao.MaterialDao;
import dev.fyloz.trial.colorrecipesexplorer.dao.MaterialRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@ -18,7 +18,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class MaterialService extends AbstractService<Material, MaterialDao> {
public class MaterialService extends AbstractJavaService<Material, MaterialRepository> {
private MixQuantityService mixQuantityService;
private SimdutService simdutService;
@ -28,8 +28,8 @@ public class MaterialService extends AbstractService<Material, MaterialDao> {
}
@Autowired
public void setMaterialDao(MaterialDao materialDao) {
this.dao = materialDao;
public void setMaterialDao(MaterialRepository materialRepository) {
this.dao = materialRepository;
}
@Autowired

View File

@ -7,8 +7,8 @@ import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.EntityNotFoundE
import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.ModelException;
import dev.fyloz.trial.colorrecipesexplorer.core.model.MaterialType;
import dev.fyloz.trial.colorrecipesexplorer.core.model.dto.MaterialTypeEditorDto;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractService;
import dev.fyloz.trial.colorrecipesexplorer.dao.MaterialTypeDao;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractJavaService;
import dev.fyloz.trial.colorrecipesexplorer.dao.MaterialTypeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -19,7 +19,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class MaterialTypeService extends AbstractService<MaterialType, MaterialTypeDao> {
public class MaterialTypeService extends AbstractJavaService<MaterialType, MaterialTypeRepository> {
private MaterialService materialService;
@ -30,8 +30,8 @@ public class MaterialTypeService extends AbstractService<MaterialType, MaterialT
}
@Autowired
public void setMaterialTypeDao(MaterialTypeDao materialTypeDao) {
this.dao = materialTypeDao;
public void setMaterialTypeDao(MaterialTypeRepository materialTypeRepository) {
this.dao = materialTypeRepository;
}
@Autowired

View File

@ -2,21 +2,21 @@ package dev.fyloz.trial.colorrecipesexplorer.core.services.model;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Material;
import dev.fyloz.trial.colorrecipesexplorer.core.model.MixQuantity;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractService;
import dev.fyloz.trial.colorrecipesexplorer.dao.MixQuantityDao;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractJavaService;
import dev.fyloz.trial.colorrecipesexplorer.dao.MixQuantityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MixQuantityService extends AbstractService<MixQuantity, MixQuantityDao> {
public class MixQuantityService extends AbstractJavaService<MixQuantity, MixQuantityRepository> {
public MixQuantityService() {
super(MixQuantity.class);
}
@Autowired
public void setMixQuantityDao(MixQuantityDao mixQuantityDao) {
this.dao = mixQuantityDao;
public void setMixQuantityDao(MixQuantityRepository mixQuantityRepository) {
this.dao = mixQuantityRepository;
}
/**

View File

@ -7,9 +7,9 @@ import dev.fyloz.trial.colorrecipesexplorer.core.model.Mix;
import dev.fyloz.trial.colorrecipesexplorer.core.model.MixType;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Recipe;
import dev.fyloz.trial.colorrecipesexplorer.core.model.dto.MixFormDto;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractService;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractJavaService;
import dev.fyloz.trial.colorrecipesexplorer.core.utils.MixBuilder;
import dev.fyloz.trial.colorrecipesexplorer.dao.MixDao;
import dev.fyloz.trial.colorrecipesexplorer.dao.MixRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -19,7 +19,7 @@ import java.util.Comparator;
import java.util.stream.Collectors;
@Service
public class MixService extends AbstractService<Mix, MixDao> {
public class MixService extends AbstractJavaService<Mix, MixRepository> {
private MaterialService materialService;
private MixQuantityService mixQuantityService;
@ -30,8 +30,8 @@ public class MixService extends AbstractService<Mix, MixDao> {
}
@Autowired
public void setMixDao(MixDao mixDao) {
this.dao = mixDao;
public void setMixDao(MixRepository mixRepository) {
this.dao = mixRepository;
}
@Autowired

View File

@ -6,8 +6,8 @@ import dev.fyloz.trial.colorrecipesexplorer.core.exception.model.ModelException;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Material;
import dev.fyloz.trial.colorrecipesexplorer.core.model.MaterialType;
import dev.fyloz.trial.colorrecipesexplorer.core.model.MixType;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractService;
import dev.fyloz.trial.colorrecipesexplorer.dao.MixTypeDao;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractJavaService;
import dev.fyloz.trial.colorrecipesexplorer.dao.MixTypeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -15,7 +15,7 @@ import javax.validation.constraints.NotNull;
import java.util.Optional;
@Service
public class MixTypeService extends AbstractService<MixType, MixTypeDao> {
public class MixTypeService extends AbstractJavaService<MixType, MixTypeRepository> {
private MaterialService materialService;
@ -24,8 +24,8 @@ public class MixTypeService extends AbstractService<MixType, MixTypeDao> {
}
@Autowired
public void setMixTypeDao(MixTypeDao mixTypeDao) {
this.dao = mixTypeDao;
public void setMixTypeDao(MixTypeRepository mixTypeRepository) {
this.dao = mixTypeRepository;
}
@Autowired

View File

@ -5,9 +5,9 @@ import dev.fyloz.trial.colorrecipesexplorer.core.model.Mix;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Recipe;
import dev.fyloz.trial.colorrecipesexplorer.core.model.dto.RecipeEditorFormDto;
import dev.fyloz.trial.colorrecipesexplorer.core.model.dto.RecipeExplorerFormDto;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractService;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractJavaService;
import dev.fyloz.trial.colorrecipesexplorer.core.services.files.ImagesService;
import dev.fyloz.trial.colorrecipesexplorer.dao.RecipeDao;
import dev.fyloz.trial.colorrecipesexplorer.dao.RecipeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -17,7 +17,7 @@ import java.util.*;
import java.util.stream.Collectors;
@Service
public class RecipeService extends AbstractService<Recipe, RecipeDao> {
public class RecipeService extends AbstractJavaService<Recipe, RecipeRepository> {
private CompanyService companyService;
private MixService mixService;
@ -29,8 +29,8 @@ public class RecipeService extends AbstractService<Recipe, RecipeDao> {
}
@Autowired
public void setRecipeDao(RecipeDao recipeDao) {
this.dao = recipeDao;
public void setRecipeDao(RecipeRepository recipeRepository) {
this.dao = recipeRepository;
}
@Autowired

View File

@ -2,8 +2,8 @@ package dev.fyloz.trial.colorrecipesexplorer.core.services.model;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Recipe;
import dev.fyloz.trial.colorrecipesexplorer.core.model.RecipeStep;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractService;
import dev.fyloz.trial.colorrecipesexplorer.dao.StepDao;
import dev.fyloz.trial.colorrecipesexplorer.core.services.AbstractJavaService;
import dev.fyloz.trial.colorrecipesexplorer.dao.StepRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -12,15 +12,15 @@ import java.util.List;
import java.util.stream.Collectors;
@Service
public class StepService extends AbstractService<RecipeStep, StepDao> {
public class StepService extends AbstractJavaService<RecipeStep, StepRepository> {
public StepService() {
super(RecipeStep.class);
}
@Autowired
public void setStepDao(StepDao stepDao) {
this.dao = stepDao;
public void setStepDao(StepRepository stepRepository) {
this.dao = stepRepository;
}
/**

View File

@ -5,7 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CompanyDao extends JpaRepository<Company, Long> {
public interface CompanyRepository extends JpaRepository<Company, Long> {
boolean existsByName(String name);
}

View File

@ -0,0 +1,16 @@
package dev.fyloz.trial.colorrecipesexplorer.dao
import dev.fyloz.trial.colorrecipesexplorer.core.model.Employee
import dev.fyloz.trial.colorrecipesexplorer.core.model.EmployeeGroup
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface EmployeeRepository : JpaRepository<Employee, Long> {
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
fun findByFirstNameAndLastName(firstName: String, lastName: String): Employee?
}
@Repository
interface EmployeeGroupRepository : JpaRepository<EmployeeGroup, Long>

View File

@ -9,7 +9,7 @@ import java.util.List;
import java.util.Optional;
@Repository
public interface MaterialDao extends JpaRepository<Material, Long> {
public interface MaterialRepository extends JpaRepository<Material, Long> {
boolean existsByName(String name);

View File

@ -5,7 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MaterialTypeDao extends JpaRepository<MaterialType, Long> {
public interface MaterialTypeRepository extends JpaRepository<MaterialType, Long> {
boolean existsByName(String name);

View File

@ -8,7 +8,7 @@ import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MixQuantityDao extends JpaRepository<MixQuantity, Long> {
public interface MixQuantityRepository extends JpaRepository<MixQuantity, Long> {
List<MixQuantity> findAllByMaterial(Material material);
boolean existsByMaterial(Material material);

View File

@ -8,7 +8,7 @@ import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MixDao extends JpaRepository<Mix, Long> {
public interface MixRepository extends JpaRepository<Mix, Long> {
List<Mix> findAllByRecipe(Recipe recipe);

View File

@ -8,7 +8,7 @@ import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface MixTypeDao extends JpaRepository<MixType, Long> {
public interface MixTypeRepository extends JpaRepository<MixType, Long> {
boolean existsByName(String name);

View File

@ -9,7 +9,7 @@ import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface RecipeDao extends JpaRepository<Recipe, Long> {
public interface RecipeRepository extends JpaRepository<Recipe, Long> {
List<Recipe> findAllByCompany(Company company);

View File

@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface StepDao extends JpaRepository<RecipeStep, Long> {
public interface StepRepository extends JpaRepository<RecipeStep, Long> {
List<RecipeStep> findAllByRecipe(Recipe recipe);

View File

@ -0,0 +1,106 @@
package dev.fyloz.trial.colorrecipesexplorer.rest
import dev.fyloz.trial.colorrecipesexplorer.core.model.Employee
import dev.fyloz.trial.colorrecipesexplorer.core.model.EmployeeDto
import dev.fyloz.trial.colorrecipesexplorer.core.model.EmployeeGroup
import dev.fyloz.trial.colorrecipesexplorer.core.model.EmployeePermission
import dev.fyloz.trial.colorrecipesexplorer.core.services.EmployeeGroupService
import dev.fyloz.trial.colorrecipesexplorer.core.services.EmployeeService
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
import java.net.URI
import java.security.Principal
import javax.validation.Valid
private const val EMPLOYEE_CONTROLLER_PATH = "api/employee"
private const val EMPLOYEE_GROUP_CONTROLLER_PATH = "api/employee/group"
@RestController
@RequestMapping(EMPLOYEE_CONTROLLER_PATH)
@Profile("rest")
class EmployeeController(employeeService: EmployeeService) :
AbstractRestModelController<Employee, EmployeeService>(employeeService, EMPLOYEE_CONTROLLER_PATH) {
@GetMapping("current")
@ResponseStatus(HttpStatus.OK)
fun getCurrent(loggedInEmployee: Principal): ResponseEntity<Employee> = getById(loggedInEmployee.name.toLong())
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun save(@Valid @RequestBody employee: EmployeeDto): ResponseEntity<Employee> {
val saved = service.save(employee)
return ResponseEntity
.created(URI("$controllerPath/${getEntityId(saved)}"))
.body(saved)
}
@PostMapping("create")
@ResponseStatus(HttpStatus.NOT_FOUND)
override fun save(entity: Employee): ResponseEntity<Employee> = ResponseEntity.notFound().build()
@PutMapping("{employeeId}/permissions/{permission}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasAnyAuthority('EDIT_EMPLOYEE')")
fun addPermission(@PathVariable employeeId: Long, @PathVariable permission: EmployeePermission): ResponseEntity<Void> {
service.addPermission(employeeId, permission)
return ResponseEntity
.noContent()
.build()
}
@DeleteMapping("{employeeId}/permissions/{permission}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasAnyAuthority('EDIT_EMPLOYEE')")
fun removePermission(@PathVariable employeeId: Long, @PathVariable permission: EmployeePermission): ResponseEntity<Void> {
service.removePermission(employeeId, permission)
return ResponseEntity
.noContent()
.build()
}
@PutMapping("{employeeId}/excludedPermissions/{excludedPermission}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasAnyAuthority('EDIT_EMPLOYEE')")
fun addExcludedPermission(@PathVariable employeeId: Long, @PathVariable excludedPermission: EmployeePermission): ResponseEntity<Void> {
service.addExcludedPermission(employeeId, excludedPermission)
return ResponseEntity
.noContent()
.build()
}
@DeleteMapping("{employeeId}/excludedPermissions/{excludedPermission}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasAnyAuthority('EDIT_EMPLOYEE')")
fun removeExcludedPermission(@PathVariable employeeId: Long, @PathVariable excludedPermission: EmployeePermission): ResponseEntity<Void> {
service.removeExcludedPermission(employeeId, excludedPermission)
return ResponseEntity
.noContent()
.build()
}
}
@RestController
@RequestMapping(EMPLOYEE_GROUP_CONTROLLER_PATH)
@Profile("rest")
class GroupsController(groupService: EmployeeGroupService) :
AbstractRestModelController<EmployeeGroup, EmployeeGroupService>(groupService, EMPLOYEE_GROUP_CONTROLLER_PATH) {
@PutMapping("{groupId}/{employeeId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun addEmployeeToGroup(@PathVariable groupId: Long, @PathVariable employeeId: Long): ResponseEntity<Void> {
service.addEmployeeToGroup(groupId, employeeId)
return ResponseEntity
.noContent()
.build()
}
@DeleteMapping("{groupId}/{employeeId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun removeEmployeeFromGroup(@PathVariable groupId: Long, @PathVariable employeeId: Long): ResponseEntity<Void> {
service.removeEmployeeFromGroup(groupId, employeeId)
return ResponseEntity
.noContent()
.build()
}
}

View File

@ -0,0 +1,81 @@
package dev.fyloz.trial.colorrecipesexplorer.rest
import dev.fyloz.trial.colorrecipesexplorer.core.model.IModel
import dev.fyloz.trial.colorrecipesexplorer.core.services.IGenericModelService
import dev.fyloz.trial.colorrecipesexplorer.core.services.IGenericService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
import java.net.URI
import javax.validation.Valid
interface IRestController<E> {
@ResponseStatus(HttpStatus.OK)
fun getAll(): ResponseEntity<Iterable<E>>
@ResponseStatus(HttpStatus.CREATED)
fun save(entity: E): ResponseEntity<E>
@ResponseStatus(HttpStatus.NO_CONTENT)
fun update(entity: E): ResponseEntity<Void>
@ResponseStatus(HttpStatus.NO_CONTENT)
fun delete(entity: E): ResponseEntity<Void>
}
interface IRestModelController<E : IModel> : IRestController<E> {
@ResponseStatus(HttpStatus.OK)
fun getById(id: Long): ResponseEntity<E>
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteById(id: Long): ResponseEntity<Void>
}
abstract class AbstractRestController<E, S : IGenericService<E>>(val service: S, protected val controllerPath: String) :
IRestController<E> {
protected abstract fun getEntityId(entity: E): Any?
@GetMapping
override fun getAll(): ResponseEntity<Iterable<E>> = ResponseEntity.ok(service.getAll())
@PostMapping
override fun save(@Valid @RequestBody entity: E): ResponseEntity<E> {
val saved = service.save(entity)
return ResponseEntity
.created(URI("$controllerPath/${getEntityId(saved)}"))
.body(saved)
}
@PutMapping
override fun update(@Valid @RequestBody entity: E): ResponseEntity<Void> {
service.update(entity)
return ResponseEntity
.noContent()
.build()
}
@DeleteMapping
override fun delete(@Valid @RequestBody entity: E): ResponseEntity<Void> {
service.delete(entity)
return ResponseEntity
.noContent()
.build()
}
}
abstract class AbstractRestModelController<E : IModel, S : IGenericModelService<E>>(service: S, controllerPath: String) :
AbstractRestController<E, S>(service, controllerPath), IRestModelController<E> {
override fun getEntityId(entity: E) = entity.id
@GetMapping("{id}")
override fun getById(@PathVariable id: Long): ResponseEntity<E> = ResponseEntity.ok(service.getById(id))
@DeleteMapping("{id}")
override fun deleteById(@PathVariable id: Long): ResponseEntity<Void> {
service.deleteById(id)
return ResponseEntity
.noContent()
.build()
}
}

View File

@ -9,6 +9,7 @@ import dev.fyloz.trial.colorrecipesexplorer.core.model.Mix;
import dev.fyloz.trial.colorrecipesexplorer.core.model.MixType;
import dev.fyloz.trial.colorrecipesexplorer.core.model.Recipe;
import dev.fyloz.trial.colorrecipesexplorer.core.model.dto.MixFormDto;
import dev.fyloz.trial.colorrecipesexplorer.core.services.ServiceKt;
import dev.fyloz.trial.colorrecipesexplorer.core.services.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
@ -52,7 +53,7 @@ public class MixCreatorController {
modelResponseBuilder
.addResponseData(ResponseDataType.RECIPE, recipe)
.addResponseData(ResponseDataType.MATERIAL_TYPES, materialTypeService.getAll())
.addResponseData(ResponseDataType.MATERIALS_JSON, materialService.asJson(mixService.getAvailableMaterialsForNewMix(recipe)));
.addResponseData(ResponseDataType.MATERIALS_JSON, ServiceKt.asJson(mixService.getAvailableMaterialsForNewMix(recipe)));
if (materialService.getAll().isEmpty())
modelResponseBuilder.addResponseData(ResponseDataType.BLOCK_BUTTON, true);

View File

@ -1,2 +1 @@
spring.resources.static-locations=classpath:/angular/static/
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration

View File

@ -0,0 +1 @@
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration

View File

@ -2,50 +2,43 @@
spring.datasource.url=jdbc:h2:file:./workdir/recipes
spring.datasource.username=sa
spring.datasource.password=LWK4Y7TvEbNyhu1yCoG3
# CONSOLE DE LA BDD
spring.h2.console.path=/dbconsole
# PORT
server.port=9090
# CRE
cre.server.upload-directory=./workdir
cre.server.password-file=passwords.txt
cre.server.url-use-port=true
cre.server.url-use-https=false
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE
cre.security.jwt-duration=18000000
cre.security.root.id=9999
cre.security.root.password=password
# TYPES DE PRODUIT PAR DÉFAUT
entities.material-types.defaults[0].name=Aucun
entities.material-types.defaults[0].prefix=
entities.material-types.defaults[0].use-percentages=false
entities.material-types.defaults[1].name=Base
entities.material-types.defaults[1].prefix=BAS
entities.material-types.defaults[1].use-percentages=false
entities.material-types.base-name=Base
# DEBUG
spring.jpa.show-sql=true
spring.h2.console.enabled=true
# Permet d'accéder à la console de la BDD à distance
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false
# NE PAS MODIFIER
spring.datasource.driver-class-name=org.h2.Driver
spring.messages.fallback-to-system-locale=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=15MB
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.open-in-view=true
server.http2.enabled=true
server.error.whitelabel.enabled=false
#spring.redis.host=localhost
#spring.redis.port=6379
spring.profiles.active=@spring.profiles.active@

View File

@ -30,4 +30,4 @@
<appender-ref ref="CONSOLE"/>
<appender-ref ref="LOG_FILE"/>
</root>
</configuration>
</configuration>