Ajout du support pour les étapes des recettes dans l'API REST.

This commit is contained in:
FyloZ 2021-01-07 17:11:21 -05:00
parent fe9dcacc7c
commit 854d3c2c3e
17 changed files with 481 additions and 267 deletions

View File

@ -1,44 +0,0 @@
package dev.fyloz.trial.colorrecipesexplorer.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import org.jetbrains.annotations.Nullable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.Objects;
@Entity
@Data
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class RecipeStep implements Model {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@NonNull
@JsonIgnore
@ManyToOne
private Recipe recipe;
@NonNull
@NotNull
private String message;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RecipeStep that = (RecipeStep) o;
return Objects.equals(recipe, that.recipe) &&
Objects.equals(message, that.message);
}
@Override
public int hashCode() {
return Objects.hash(recipe, message);
}
}

View File

@ -1,13 +0,0 @@
package dev.fyloz.trial.colorrecipesexplorer.repository;
import dev.fyloz.trial.colorrecipesexplorer.model.Recipe;
import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStep;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface StepRepository extends JpaRepository<RecipeStep, Long> {
List<RecipeStep> findAllByRecipe(Recipe recipe);
}

View File

@ -21,7 +21,7 @@ public class RecipeService extends AbstractJavaService<Recipe, RecipeRepository>
private CompanyJavaService companyService;
private MixService mixService;
private StepService stepService;
private RecipeStepJavaService stepService;
private ImagesService imagesService;
public RecipeService() {
@ -44,7 +44,7 @@ public class RecipeService extends AbstractJavaService<Recipe, RecipeRepository>
}
@Autowired
public void setStepService(StepService stepService) {
public void setStepService(RecipeStepJavaService stepService) {
this.stepService = stepService;
}

View File

@ -2,8 +2,8 @@ package dev.fyloz.trial.colorrecipesexplorer.service.model;
import dev.fyloz.trial.colorrecipesexplorer.model.Recipe;
import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStep;
import dev.fyloz.trial.colorrecipesexplorer.repository.RecipeStepRepository;
import dev.fyloz.trial.colorrecipesexplorer.service.AbstractJavaService;
import dev.fyloz.trial.colorrecipesexplorer.repository.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 AbstractJavaService<RecipeStep, StepRepository> {
public class RecipeStepJavaService extends AbstractJavaService<RecipeStep, RecipeStepRepository> {
public StepService() {
public RecipeStepJavaService() {
super(RecipeStep.class);
}
@Autowired
public void setStepDao(StepRepository stepRepository) {
this.repository = stepRepository;
public void setStepDao(RecipeStepRepository recipeStepRepository) {
this.repository = recipeStepRepository;
}
/**

View File

@ -53,9 +53,10 @@ import javax.servlet.http.HttpServletResponse
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableConfigurationProperties(SecurityConfigurationProperties::class)
class WebSecurityConfig(
val restAuthenticationEntryPoint: RestAuthenticationEntryPoint,
val securityConfigurationProperties: SecurityConfigurationProperties,
val logger: Logger) : WebSecurityConfigurerAdapter() {
val restAuthenticationEntryPoint: RestAuthenticationEntryPoint,
val securityConfigurationProperties: SecurityConfigurationProperties,
val logger: Logger
) : WebSecurityConfigurerAdapter() {
@Autowired
private lateinit var userDetailsService: EmployeeUserDetailsServiceImpl
@ -82,12 +83,12 @@ class WebSecurityConfig(
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
HttpMethod.GET.name,
HttpMethod.POST.name,
HttpMethod.PUT.name,
HttpMethod.DELETE.name,
HttpMethod.OPTIONS.name,
HttpMethod.HEAD.name
)
allowCredentials = true
}.applyPermitDefaultValues())
@ -96,20 +97,27 @@ class WebSecurityConfig(
@PostConstruct
fun createSystemUsers() {
fun createUser(credentials: SecurityConfigurationProperties.SystemUserCredentials?, firstName: String, lastName: String, permissions: List<EmployeePermission>) {
fun createUser(
credentials: SecurityConfigurationProperties.SystemUserCredentials?,
firstName: String,
lastName: String,
permissions: List<EmployeePermission>
) {
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(
employeeService.save(
Employee(
id = credentials.id!!,
firstName = firstName,
lastName = lastName,
password = passwordEncoder().encode(credentials.password!!),
isSystemUser = true,
permissions = permissions.toMutableSet()
))
)
)
}
}
@ -130,27 +138,43 @@ class WebSecurityConfig(
}
http
.cors()
.and()
.headers().frameOptions().disable()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/").permitAll()
.antMatchers("/api/login").permitAll()
.antMatchers("/api/employee/logout").permitAll()
.antMatchers(HttpMethod.GET, "/api/employee/current").authenticated()
.generateAuthorizations()
.and()
.addFilter(JwtAuthenticationFilter(authenticationManager(), employeeService, securityConfigurationProperties))
.addFilter(JwtAuthorizationFilter(userDetailsService, securityConfigurationProperties, authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.cors()
.and()
.headers().frameOptions().disable()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/").permitAll()
.antMatchers("/api/login").permitAll()
.antMatchers("/api/employee/logout").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")
override fun commence(
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException
) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
}
class CorsFilter : Filter {
@ -172,9 +196,9 @@ const val defaultGroupCookieName = "Default-Group"
val blacklistedJwtTokens = mutableListOf<String>()
class JwtAuthenticationFilter(
val authManager: AuthenticationManager,
val employeeService: EmployeeServiceImpl,
val securityConfigurationProperties: SecurityConfigurationProperties
val authManager: AuthenticationManager,
val employeeService: EmployeeServiceImpl,
val securityConfigurationProperties: SecurityConfigurationProperties
) : UsernamePasswordAuthenticationFilter() {
init {
setFilterProcessesUrl("/api/login")
@ -185,7 +209,12 @@ class JwtAuthenticationFilter(
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
}
override fun successfulAuthentication(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain, authResult: Authentication) {
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.")
@ -195,25 +224,29 @@ class JwtAuthenticationFilter(
val expirationMs = System.currentTimeMillis() + jwtDuration!!
val expirationDate = Date(expirationMs)
val token = Jwts.builder()
.setSubject(employeeId)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
.compact()
.setSubject(employeeId)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
.compact()
response.addHeader("Access-Control-Expose-Headers", "X-Authentication-Expiration")
response.addHeader("Set-Cookie", "$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; Secure; SameSite=strict")
response.addHeader(
"Set-Cookie",
"$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; Secure; SameSite=strict"
)
response.addHeader(authorizationCookieName, "Bearer $token")
response.addHeader("X-Authentication-Expiration", "$expirationMs")
}
}
class JwtAuthorizationFilter(
val userDetailsService: EmployeeUserDetailsServiceImpl,
val securityConfigurationProperties: SecurityConfigurationProperties,
authenticationManager: AuthenticationManager
val userDetailsService: EmployeeUserDetailsServiceImpl,
val securityConfigurationProperties: SecurityConfigurationProperties,
authenticationManager: AuthenticationManager
) : BasicAuthenticationFilter(authenticationManager) {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
val authorizationValue = if (authorizationCookie != null) authorizationCookie.value else request.getHeader(authorizationCookieName)
val authorizationValue =
if (authorizationCookie != null) authorizationCookie.value else request.getHeader(authorizationCookieName)
if (authorizationValue != null && authorizationValue.startsWith("Bearer") && authorizationValue !in blacklistedJwtTokens) {
val authenticationToken = getAuthentication(authorizationValue)
SecurityContextHolder.getContext().authentication = authenticationToken
@ -231,10 +264,10 @@ class JwtAuthorizationFilter(
val jwtSecret = securityConfigurationProperties.jwtSecret
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
val employeeId = Jwts.parser()
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
return if (employeeId != null) getAuthenticationToken(employeeId) else null
}
@ -254,47 +287,71 @@ class SecurityConfigurationProperties {
}
private enum class ControllerAuthorizations(
val antMatcher: String,
val permissions: Map<HttpMethod, EmployeePermission>
val antMatcher: String,
val permissions: Map<HttpMethod, EmployeePermission>
) {
MATERIALS("/api/material/**", mapOf(
MATERIALS(
"/api/material/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_MATERIAL,
HttpMethod.POST to EmployeePermission.EDIT_MATERIAL,
HttpMethod.PUT to EmployeePermission.EDIT_MATERIAL,
HttpMethod.DELETE to EmployeePermission.REMOVE_MATERIAL
)),
MATERIAL_TYPES("/api/materialtype/**", mapOf(
)
),
MATERIAL_TYPES(
"/api/materialtype/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_MATERIAL_TYPE,
HttpMethod.POST to EmployeePermission.EDIT_MATERIAL_TYPE,
HttpMethod.PUT to EmployeePermission.EDIT_MATERIAL_TYPE,
HttpMethod.DELETE to EmployeePermission.REMOVE_MATERIAL_TYPE
)),
COMPANY("/api/company/**", mapOf(
)
),
COMPANY(
"/api/company/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_COMPANY,
HttpMethod.POST to EmployeePermission.EDIT_COMPANY,
HttpMethod.PUT to EmployeePermission.EDIT_COMPANY,
HttpMethod.DELETE to EmployeePermission.REMOVE_COMPANY
)),
SET_BROWSER_DEFAULT_GROUP("/api/employee/group/default/**", mapOf(
)
),
RECIPE_STEP(
"/api/recipe/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_RECIPE,
HttpMethod.POST to EmployeePermission.EDIT_RECIPE,
HttpMethod.PUT to EmployeePermission.EDIT_RECIPE,
HttpMethod.DELETE to EmployeePermission.REMOVE_RECIPE
)
),
SET_BROWSER_DEFAULT_GROUP(
"/api/employee/group/default/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE_GROUP,
HttpMethod.POST to EmployeePermission.SET_BROWSER_DEFAULT_GROUP
)),
EMPLOYEES_FOR_GROUP("/api/employee/group/*/employees", mapOf(
)
),
EMPLOYEES_FOR_GROUP(
"/api/employee/group/*/employees", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE
)),
EMPLOYEE_GROUP("/api/employee/group/**", mapOf(
)
),
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_PASSWORD("/api/employee/*/password", mapOf(
)
),
EMPLOYEE_PASSWORD(
"/api/employee/*/password", mapOf(
HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE_PASSWORD
)),
EMPLOYEE("/api/employee/**", mapOf(
)
),
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

@ -24,82 +24,93 @@ private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit co
@Entity
data class Employee(
@Id
override val id: Long,
@Id
override val id: Long,
val firstName: String = "",
val firstName: String = "",
val lastName: String = "",
val lastName: String = "",
@JsonIgnore
val password: String = "",
@JsonIgnore
val password: String = "",
@JsonIgnore
val isDefaultGroupUser: Boolean = false,
@JsonIgnore
val isDefaultGroupUser: Boolean = false,
@JsonIgnore
val isSystemUser: Boolean = false,
@JsonIgnore
val isSystemUser: Boolean = false,
@field:ManyToOne
@Fetch(FetchMode.SELECT)
var group: EmployeeGroup? = null,
@field:ManyToOne
@Fetch(FetchMode.SELECT)
var group: EmployeeGroup? = null,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@Fetch(FetchMode.SUBSELECT)
@get:JsonIgnore
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@Fetch(FetchMode.SUBSELECT)
@get:JsonIgnore
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
val lastLoginTime: LocalDateTime? = null
val lastLoginTime: LocalDateTime? = null
) : Model {
@JsonProperty("permissions")
fun getFlattenedPermissions(): Iterable<EmployeePermission> = getPermissions()
override fun equals(other: Any?): Boolean = other is Employee && id == other.id && firstName == other.firstName && lastName == other.lastName
override fun equals(other: Any?): Boolean =
other is Employee && id == other.id && firstName == other.firstName && lastName == other.lastName
override fun hashCode(): Int = Objects.hash(id, firstName, lastName)
}
/** DTO for creating employees. Allows a [password] a [groupId]. */
open class EmployeeSaveDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@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_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String,
@field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: 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: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 groupId: Long? = null,
@field:ManyToOne
@Fetch(FetchMode.SELECT)
var groupId: Long? = null,
@Enumerated(EnumType.STRING)
val permissions: MutableSet<EmployeePermission> = mutableSetOf()
@Enumerated(EnumType.STRING)
val permissions: MutableSet<EmployeePermission> = mutableSetOf()
) : EntityDto<Employee> {
override fun toEntity(): Employee =
Employee(id, firstName, lastName, "", isDefaultGroupUser = false, isSystemUser = false, group = null, permissions = permissions)
Employee(
id,
firstName,
lastName,
"",
isDefaultGroupUser = false,
isSystemUser = false,
group = null,
permissions = permissions
)
}
open class EmployeeUpdateDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@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_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String = "",
@field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String = "",
@field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String = "",
@Enumerated(EnumType.STRING)
val permissions: Set<EmployeePermission> = mutableSetOf()
@Enumerated(EnumType.STRING)
val permissions: Set<EmployeePermission> = mutableSetOf()
) : EntityDto<Employee> {
override fun toEntity(): Employee =
Employee(id, firstName, lastName, permissions = permissions.toMutableSet())
Employee(id, firstName, lastName, permissions = permissions.toMutableSet())
}
@ -109,20 +120,20 @@ private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est
@Entity
data class EmployeeGroup(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
override var id: Long? = null,
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
override var id: Long? = null,
@Column(unique = true)
override val name: String = "",
@Column(unique = true)
override val name: String = "",
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
@OneToMany
@JsonIgnore
val employees: MutableSet<Employee> = mutableSetOf()
@OneToMany
@JsonIgnore
val employees: MutableSet<Employee> = mutableSetOf()
) : NamedModel {
fun getEmployeeCount() = employees.size
@ -131,30 +142,30 @@ data class EmployeeGroup(
}
open class EmployeeGroupSaveDto(
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(null, name, permissions)
EmployeeGroup(null, name, permissions)
}
open class EmployeeGroupUpdateDto(
@field:NotNull(message = GROUP_ID_NULL_MESSAGE)
val id: Long,
@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:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(id, name, permissions)
EmployeeGroup(id, name, permissions)
}
@ -166,11 +177,15 @@ enum class EmployeePermission(val impliedPermissions: List<EmployeePermission> =
VIEW_MATERIAL,
VIEW_MATERIAL_TYPE,
VIEW_COMPANY,
VIEW(listOf(
VIEW_RECIPE,
VIEW(
listOf(
VIEW_MATERIAL,
VIEW_MATERIAL_TYPE,
VIEW_COMPANY
)),
VIEW_COMPANY,
VIEW_RECIPE
)
),
VIEW_EMPLOYEE,
VIEW_EMPLOYEE_GROUP,
@ -178,12 +193,16 @@ enum class EmployeePermission(val impliedPermissions: List<EmployeePermission> =
EDIT_MATERIAL(listOf(VIEW_MATERIAL)),
EDIT_MATERIAL_TYPE(listOf(VIEW_MATERIAL_TYPE)),
EDIT_COMPANY(listOf(VIEW_COMPANY)),
EDIT(listOf(
EDIT_RECIPE(listOf(VIEW_RECIPE)),
EDIT(
listOf(
EDIT_MATERIAL,
EDIT_MATERIAL_TYPE,
EDIT_COMPANY,
EDIT_RECIPE,
VIEW
)),
)
),
EDIT_EMPLOYEE(listOf(VIEW_EMPLOYEE)),
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_EMPLOYEE)),
EDIT_EMPLOYEE_GROUP(listOf(VIEW_EMPLOYEE_GROUP)),
@ -192,21 +211,28 @@ enum class EmployeePermission(val impliedPermissions: List<EmployeePermission> =
REMOVE_MATERIAL(listOf(EDIT_MATERIAL)),
REMOVE_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPE)),
REMOVE_COMPANY(listOf(EDIT_COMPANY)),
REMOVE(listOf(
REMOVE_RECIPE(listOf(EDIT_RECIPE)),
REMOVE(
listOf(
REMOVE_MATERIAL,
REMOVE_MATERIAL_TYPE,
REMOVE_COMPANY,
REMOVE_RECIPE,
EDIT
)),
)
),
REMOVE_EMPLOYEE(listOf(EDIT_EMPLOYEE)),
REMOVE_EMPLOYEE_GROUP(listOf(EDIT_EMPLOYEE_GROUP)),
// Others
SET_BROWSER_DEFAULT_GROUP(listOf(
SET_BROWSER_DEFAULT_GROUP(
listOf(
VIEW_EMPLOYEE_GROUP
)),
)
),
ADMIN(listOf(
ADMIN(
listOf(
REMOVE,
SET_BROWSER_DEFAULT_GROUP,
@ -214,7 +240,8 @@ enum class EmployeePermission(val impliedPermissions: List<EmployeePermission> =
REMOVE_EMPLOYEE,
EDIT_EMPLOYEE_PASSWORD,
REMOVE_EMPLOYEE_GROUP,
));
)
);
operator fun contains(permission: EmployeePermission): Boolean {
return permission == this || impliedPermissions.any { permission in it }
@ -250,70 +277,82 @@ private fun EmployeePermission.toAuthority(): GrantedAuthority {
// ==== DSL ====
fun employee(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: EmployeeGroup? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: Employee.() -> Unit = {}
) = Employee(id, firstName, lastName, password, isDefaultGroupUser, isSystemUser, group, permissions, lastLoginTime).apply(op)
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: EmployeeGroup? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: Employee.() -> Unit = {}
) = Employee(
id,
firstName,
lastName,
password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun employee(
employee: Employee,
newId: Long? = null
employee: Employee,
newId: Long? = null
) = with(employee) {
Employee(newId
?: id, firstName, lastName, password, isDefaultGroupUser, isSystemUser, group, permissions, lastLoginTime)
Employee(
newId
?: id, firstName, lastName, password, isDefaultGroupUser, isSystemUser, group, permissions, lastLoginTime
)
}
fun employeeSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeSaveDto.() -> Unit = {}
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeSaveDto.() -> Unit = {}
) = EmployeeSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
fun employeeUpdateDto(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeUpdateDto.() -> Unit = {}
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeUpdateDto.() -> Unit = {}
) = EmployeeUpdateDto(id, firstName, lastName, permissions).apply(op)
fun employeeGroup(
id: Long? = null,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
employees: MutableSet<Employee> = mutableSetOf(),
op: EmployeeGroup.() -> Unit = {}
id: Long? = null,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
employees: MutableSet<Employee> = mutableSetOf(),
op: EmployeeGroup.() -> Unit = {}
) = EmployeeGroup(id, name, permissions, employees).apply(op)
fun employeeGroup(
employeeGroup: EmployeeGroup,
newId: Long? = null,
newName: String? = null
employeeGroup: EmployeeGroup,
newId: Long? = null,
newName: String? = null
) = with(employeeGroup) { EmployeeGroup(newId ?: id, newName ?: name, permissions, employees) }
fun employeeGroupSaveDto(
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupSaveDto.() -> Unit = {}
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupSaveDto.() -> Unit = {}
) = EmployeeGroupSaveDto(name, permissions).apply(op)
fun employeeGroupUpdateDto(
id: Long = 0L,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupUpdateDto.() -> Unit = {}
id: Long = 0L,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupUpdateDto.() -> Unit = {}
) = EmployeeGroupUpdateDto(id, name, permissions).apply(op)

View File

@ -1,5 +1,6 @@
package dev.fyloz.trial.colorrecipesexplorer.model
/** The model of a stored entity. Each model should implements its own equals and hashCode methods to keep compatibility with the legacy Java and Thymeleaf code. */
interface Model {
val id: Long?
}

View File

@ -0,0 +1,76 @@
package dev.fyloz.trial.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.trial.colorrecipesexplorer.model.validation.NullOrNotBlank
import java.util.*
import javax.persistence.*
import javax.validation.constraints.NotNull
private const val RECIPE_STEP_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val RECIPE_STEP_RECIPE_NULL_MESSAGE = "Une recette est requise"
private const val RECIPE_STEP_MESSAGE_NULL_MESSAGE = "Un message est requis"
@Entity
data class RecipeStep(
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
override val id: Long?,
@JsonIgnore
@ManyToOne
val recipe: Recipe?,
val message: String
) : Model {
constructor(recipe: Recipe?, message: String) : this(null, recipe, message)
override fun equals(other: Any?): Boolean =
other is RecipeStep && other.recipe == recipe && other.message == message
override fun hashCode(): Int = Objects.hash(recipe, message)
}
open class RecipeStepSaveDto(
@field:NotNull(message = RECIPE_STEP_RECIPE_NULL_MESSAGE)
val recipe: Recipe,
@field:NotNull(message = RECIPE_STEP_MESSAGE_NULL_MESSAGE)
val message: String
) : EntityDto<RecipeStep> {
override fun toEntity(): RecipeStep = RecipeStep(null, recipe, message)
}
open class RecipeStepUpdateDto(
@field:NotNull(message = RECIPE_STEP_ID_NULL_MESSAGE)
val id: Long,
val recipe: Recipe?,
@field:NullOrNotBlank(message = RECIPE_STEP_MESSAGE_NULL_MESSAGE)
val message: String?
) : EntityDto<RecipeStep> {
override fun toEntity(): RecipeStep = RecipeStep(id, recipe, message ?: "")
}
// ==== DSL ====
fun recipeStep(
id: Long? = null,
recipe: Recipe? = TODO(), // TODO change default when recipe DSL is done
message: String = "message",
op: RecipeStep.() -> Unit = {}
) = RecipeStep(id, recipe, message).apply(op)
fun recipeStepSaveDto(
recipe: Recipe = TODO(), // TODO change default when recipe DSL is done
message: String = "message",
op: RecipeStepSaveDto.() -> Unit = {}
) = RecipeStepSaveDto(recipe, message).apply(op)
fun recipeStepUpdateDto(
id: Long = 0L,
recipe: Recipe? = TODO(),
message: String? = "message",
op: RecipeStepUpdateDto.() -> Unit = {}
) = RecipeStepUpdateDto(id, recipe, message).apply(op)

View File

@ -0,0 +1,8 @@
package dev.fyloz.trial.colorrecipesexplorer.repository
import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStep
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface RecipeStepRepository : JpaRepository<RecipeStep, Long>

View File

@ -13,4 +13,8 @@ private const val COMPANY_CONTROLLER_PATH = "api/company"
@RestController
@RequestMapping(COMPANY_CONTROLLER_PATH)
@Profile("rest")
class CompanyController(companyService: CompanyService) : AbstractRestModelApiController<Company, CompanySaveDto, CompanyUpdateDto, CompanyService>(companyService, COMPANY_CONTROLLER_PATH)
class CompanyController(companyService: CompanyService) :
AbstractRestModelApiController<Company, CompanySaveDto, CompanyUpdateDto, CompanyService>(
companyService,
COMPANY_CONTROLLER_PATH
)

View File

@ -1,11 +1,11 @@
package dev.fyloz.trial.colorrecipesexplorer.rest
import dev.fyloz.trial.colorrecipesexplorer.model.InventoryMaterial
import dev.fyloz.trial.colorrecipesexplorer.service.InventoryService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
@RestController
class InventoryController(val inventoryService: InventoryService) {
fun getAllMaterials(): ResponseEntity<Collection<InventoryMaterial>> = ResponseEntity.ok(inventoryService.getAllMaterials())
}
//package dev.fyloz.trial.colorrecipesexplorer.rest
//
//import dev.fyloz.trial.colorrecipesexplorer.model.InventoryMaterial
//import dev.fyloz.trial.colorrecipesexplorer.service.InventoryService
//import org.springframework.http.ResponseEntity
//import org.springframework.web.bind.annotation.RestController
//
//@RestController
//class InventoryController(val inventoryService: InventoryService) {
// fun getAllMaterials(): ResponseEntity<Collection<InventoryMaterial>> = ResponseEntity.ok(inventoryService.getAllMaterials())
//}

View File

@ -13,8 +13,5 @@ private const val MATERIAL_TYPE_CONTROLLER_PATH = "api/materialtype"
@RestController
@RequestMapping(MATERIAL_TYPE_CONTROLLER_PATH)
@Profile("rest")
class MaterialTypeController(materialTypeService: MaterialTypeService) :
AbstractRestModelApiController<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeService>(materialTypeService, MATERIAL_TYPE_CONTROLLER_PATH) {
}
AbstractRestModelApiController<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeService>(materialTypeService, MATERIAL_TYPE_CONTROLLER_PATH)

View File

@ -4,6 +4,7 @@ import dev.fyloz.trial.colorrecipesexplorer.model.EntityDto
import dev.fyloz.trial.colorrecipesexplorer.model.Model
import dev.fyloz.trial.colorrecipesexplorer.service.ModelService
import dev.fyloz.trial.colorrecipesexplorer.service.Service
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

View File

@ -0,0 +1,29 @@
package dev.fyloz.trial.colorrecipesexplorer.service
import dev.fyloz.trial.colorrecipesexplorer.model.Recipe
import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStep
import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStepSaveDto
import dev.fyloz.trial.colorrecipesexplorer.model.RecipeStepUpdateDto
import dev.fyloz.trial.colorrecipesexplorer.repository.RecipeStepRepository
import org.springframework.stereotype.Service
import javax.transaction.Transactional
interface RecipeStepService : ModelService<RecipeStep, RecipeStepSaveDto, RecipeStepUpdateDto, RecipeStepRepository> {
/** Creates a step for the given [recipe] with the given [message]. */
fun createForRecipe(recipe: Recipe, message: String): RecipeStep
/** Creates several steps for the given [recipe] with the given [messages]. */
fun createAllForRecipe(recipe: Recipe, messages: Collection<String>): Collection<RecipeStep>
}
@Service
class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) :
AbstractModelService<RecipeStep, RecipeStepSaveDto, RecipeStepUpdateDto, RecipeStepRepository>(recipeStepRepository),
RecipeStepService {
override fun createForRecipe(recipe: Recipe, message: String): RecipeStep =
RecipeStep(recipe, message)
@Transactional
override fun createAllForRecipe(recipe: Recipe, messages: Collection<String>): Collection<RecipeStep> =
messages.map { createForRecipe(recipe, it) }
}

View File

@ -0,0 +1,13 @@
package dev.fyloz.trial.colorrecipesexplorer.repository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
@DataJpaTest
class RecipeStepRepositoryTest @Autowired constructor(
recipeStepRepository: RecipeStepRepository,
entityManager: TestEntityManager
) {
// Nothing for now
}

View File

@ -0,0 +1,46 @@
package dev.fyloz.trial.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.spy
import dev.fyloz.trial.colorrecipesexplorer.model.*
import dev.fyloz.trial.colorrecipesexplorer.repository.RecipeStepRepository
import org.junit.jupiter.api.Nested
import kotlin.test.assertEquals
class RecipeStepServiceTest :
AbstractModelServiceTest<RecipeStep, RecipeStepSaveDto, RecipeStepUpdateDto, RecipeStepService, RecipeStepRepository>() {
override val repository: RecipeStepRepository = mock()
override val service: RecipeStepService = spy(RecipeStepServiceImpl(repository))
override val entity: RecipeStep = recipeStep(id = 0L, recipe = TODO(), message = "message")
override val anotherEntity: RecipeStep = recipeStep(id = 1L, recipe = TODO(), message = "another message")
override val entitySaveDto: RecipeStepSaveDto = spy(recipeStepSaveDto(entity.recipe!!, entity.message))
override val entityUpdateDto: RecipeStepUpdateDto =
spy(recipeStepUpdateDto(entity.id!!, entity.recipe, entity.message))
@Nested
inner class CreateForRecipe {
fun `returns a correct RecipeStep`() {
val step = recipeStep(null, entity.recipe, entity.message)
val found = service.createForRecipe(entity.recipe!!, entity.message)
assertEquals(step, found)
}
}
@Nested
inner class CreateAllForRecipe {
fun `returns all correct RecipeSteps`() {
val steps = listOf(
recipeStep(null, entity.recipe, entity.message),
recipeStep(null, entity.recipe, anotherEntity.message)
)
val messages = steps.map { it.message }
val found = service.createAllForRecipe(entity.recipe!!, messages)
assertEquals(steps, found)
}
}
}