Merge branch 'better-permissions' into 'master'
Resolve "Meilleure gestion des permissions" Closes #57 and #56 See merge request color-recipes-explorer/backend!22
This commit is contained in:
commit
2753ffde1b
|
@ -26,21 +26,18 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
|
|||
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 org.springframework.web.util.WebUtils
|
||||
import java.util.*
|
||||
|
@ -67,18 +64,16 @@ class WebSecurityConfig(
|
|||
}
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder(): PasswordEncoder {
|
||||
return BCryptPasswordEncoder()
|
||||
}
|
||||
fun passwordEncoder() =
|
||||
BCryptPasswordEncoder()
|
||||
|
||||
@Bean
|
||||
override fun authenticationManagerBean(): AuthenticationManager {
|
||||
return super.authenticationManagerBean()
|
||||
}
|
||||
override fun authenticationManagerBean(): AuthenticationManager =
|
||||
super.authenticationManagerBean()
|
||||
|
||||
@Bean
|
||||
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||
return UrlBasedCorsConfigurationSource().apply {
|
||||
fun corsConfigurationSource() =
|
||||
UrlBasedCorsConfigurationSource().apply {
|
||||
registerCorsConfiguration("/**", CorsConfiguration().apply {
|
||||
allowedOrigins = listOf("http://localhost:4200") // Angular development server
|
||||
allowedMethods = listOf(
|
||||
|
@ -92,7 +87,6 @@ class WebSecurityConfig(
|
|||
allowCredentials = true
|
||||
}.applyPermitDefaultValues())
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
fun initWebSecurity() {
|
||||
|
@ -126,18 +120,6 @@ class WebSecurityConfig(
|
|||
}
|
||||
|
||||
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
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
|
@ -160,11 +142,10 @@ class WebSecurityConfig(
|
|||
|
||||
if (!debugMode) {
|
||||
http.authorizeRequests()
|
||||
.antMatchers(HttpMethod.GET, "/").permitAll()
|
||||
.antMatchers("/api/login").permitAll()
|
||||
.antMatchers("/api/employee/logout").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/api/employee/current").authenticated()
|
||||
.generateAuthorizations()
|
||||
.antMatchers("/api/logout").authenticated()
|
||||
.antMatchers("/api/employee/current").authenticated()
|
||||
.anyRequest().authenticated()
|
||||
} else {
|
||||
http
|
||||
.cors()
|
||||
|
@ -304,83 +285,3 @@ class SecurityConfigurationProperties {
|
|||
|
||||
class SystemUserCredentials(var id: Long? = null, var password: String? = null)
|
||||
}
|
||||
|
||||
private enum class ControllerAuthorizations(
|
||||
val antMatcher: String,
|
||||
val permissions: Map<HttpMethod, EmployeePermission>
|
||||
) {
|
||||
INVENTORY_ADD(
|
||||
"/api/inventory/add/**", mapOf(
|
||||
HttpMethod.PUT to EmployeePermission.EDIT_MATERIAL
|
||||
)
|
||||
),
|
||||
INVENTORY_DEDUCT(
|
||||
"/api/inventory/deduct/**", mapOf(
|
||||
HttpMethod.PUT to EmployeePermission.VIEW_MATERIAL
|
||||
)
|
||||
),
|
||||
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(
|
||||
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(
|
||||
HttpMethod.GET to EmployeePermission.VIEW_COMPANY,
|
||||
HttpMethod.POST to EmployeePermission.EDIT_COMPANY,
|
||||
HttpMethod.PUT to EmployeePermission.EDIT_COMPANY,
|
||||
HttpMethod.DELETE to EmployeePermission.REMOVE_COMPANY
|
||||
)
|
||||
),
|
||||
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(
|
||||
HttpMethod.GET to EmployeePermission.VIEW_EMPLOYEE
|
||||
)
|
||||
),
|
||||
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(
|
||||
HttpMethod.PUT to EmployeePermission.EDIT_EMPLOYEE_PASSWORD
|
||||
)
|
||||
),
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.annotations
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@PreAuthorize("hasAuthority('VIEW_RECIPES')")
|
||||
annotation class PreAuthorizeViewRecipes
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@PreAuthorize("hasAuthority('EDIT_RECIPES')")
|
||||
annotation class PreAuthorizeEditRecipes
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@PreAuthorize("hasAuthority('REMOVE_RECIPES')")
|
||||
annotation class PreAuthorizeRemoveRecipes
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@PreAuthorize("hasAuthority('VIEW_CATALOG')")
|
||||
annotation class PreAuthorizeViewCatalog
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@PreAuthorize("hasAuthority('VIEW_USERS')")
|
||||
annotation class PreAuthorizeViewUsers
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@PreAuthorize("hasAuthority('EDIT_USERS')")
|
||||
annotation class PreAuthorizeEditUsers
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@PreAuthorize("hasAuthority('REMOVE_USERS')")
|
||||
annotation class PreAuthorizeRemoveUsers
|
|
@ -61,8 +61,19 @@ data class Employee(
|
|||
@Column(name = "last_login_time")
|
||||
var lastLoginTime: LocalDateTime? = null
|
||||
) : Model {
|
||||
@JsonProperty("permissions")
|
||||
fun getFlattenedPermissions(): Iterable<EmployeePermission> = getPermissions()
|
||||
@get:JsonProperty("permissions")
|
||||
val flatPermissions: Set<EmployeePermission>
|
||||
get() = permissions
|
||||
.flatMap { it.flat() }
|
||||
.filter { !it.deprecated }
|
||||
.toMutableSet()
|
||||
.apply {
|
||||
if (group != null) this.addAll(group!!.flatPermissions)
|
||||
}
|
||||
|
||||
@get:JsonIgnore
|
||||
val authorities: Set<GrantedAuthority>
|
||||
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
|
||||
}
|
||||
|
||||
/** DTO for creating employees. Allows a [password] a [groupId]. */
|
||||
|
@ -80,25 +91,11 @@ open class EmployeeSaveDto(
|
|||
@field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE)
|
||||
val password: String,
|
||||
|
||||
@field:ManyToOne
|
||||
@Fetch(FetchMode.SELECT)
|
||||
var groupId: Long? = null,
|
||||
val groupId: Long?,
|
||||
|
||||
@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
|
||||
)
|
||||
}
|
||||
) : EntityDto<Employee>
|
||||
|
||||
open class EmployeeUpdateDto(
|
||||
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
|
||||
|
@ -110,6 +107,8 @@ open class EmployeeUpdateDto(
|
|||
@field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
|
||||
val lastName: String?,
|
||||
|
||||
val groupId: Long?,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
val permissions: Set<EmployeePermission>?
|
||||
) : EntityDto<Employee>
|
||||
|
@ -134,8 +133,16 @@ data class EmployeeGroup(
|
|||
@CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")])
|
||||
@Column(name = "permission")
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
@get:JsonProperty("explicitPermissions")
|
||||
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
|
||||
) : NamedModel
|
||||
) : NamedModel {
|
||||
@get:JsonProperty("permissions")
|
||||
val flatPermissions: Set<EmployeePermission>
|
||||
get() = this.permissions
|
||||
.flatMap { it.flat() }
|
||||
.filter { !it.deprecated }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
open class EmployeeGroupSaveDto(
|
||||
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
|
||||
|
@ -168,96 +175,83 @@ open class EmployeeGroupUpdateDto(
|
|||
data class EmployeeLoginRequest(val id: Long, val password: String)
|
||||
|
||||
|
||||
enum class EmployeePermission(val impliedPermissions: List<EmployeePermission> = listOf()) {
|
||||
// View
|
||||
VIEW_MATERIAL,
|
||||
VIEW_MATERIAL_TYPE,
|
||||
VIEW_COMPANY,
|
||||
VIEW_RECIPE,
|
||||
VIEW(
|
||||
listOf(
|
||||
VIEW_MATERIAL,
|
||||
VIEW_MATERIAL_TYPE,
|
||||
VIEW_COMPANY,
|
||||
VIEW_RECIPE
|
||||
)
|
||||
),
|
||||
VIEW_EMPLOYEE,
|
||||
VIEW_EMPLOYEE_GROUP,
|
||||
enum class EmployeePermission(
|
||||
val impliedPermissions: List<EmployeePermission> = listOf(),
|
||||
val deprecated: Boolean = false
|
||||
) {
|
||||
VIEW_RECIPES,
|
||||
VIEW_CATALOG,
|
||||
VIEW_USERS,
|
||||
|
||||
// Edit
|
||||
EDIT_MATERIAL(listOf(VIEW_MATERIAL)),
|
||||
EDIT_MATERIAL_TYPE(listOf(VIEW_MATERIAL_TYPE)),
|
||||
EDIT_COMPANY(listOf(VIEW_COMPANY)),
|
||||
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)),
|
||||
PRINT_MIXES(listOf(VIEW_RECIPES)),
|
||||
|
||||
// Remove
|
||||
REMOVE_MATERIAL(listOf(EDIT_MATERIAL)),
|
||||
REMOVE_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPE)),
|
||||
REMOVE_COMPANY(listOf(EDIT_COMPANY)),
|
||||
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)),
|
||||
EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)),
|
||||
EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA)),
|
||||
EDIT_MATERIALS(listOf(VIEW_CATALOG)),
|
||||
EDIT_MATERIAL_TYPES(listOf(VIEW_CATALOG)),
|
||||
EDIT_COMPANIES(listOf(VIEW_CATALOG)),
|
||||
EDIT_USERS(listOf(VIEW_USERS)),
|
||||
EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)),
|
||||
|
||||
// Others
|
||||
SET_BROWSER_DEFAULT_GROUP(
|
||||
listOf(
|
||||
VIEW_EMPLOYEE_GROUP
|
||||
)
|
||||
),
|
||||
ADD_TO_INVENTORY(listOf(VIEW_CATALOG)),
|
||||
DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)),
|
||||
|
||||
REMOVE_RECIPES(listOf(EDIT_RECIPES)),
|
||||
REMOVE_MATERIALS(listOf(EDIT_MATERIALS)),
|
||||
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES)),
|
||||
REMOVE_COMPANIES(listOf(EDIT_COMPANIES)),
|
||||
REMOVE_USERS(listOf(EDIT_USERS)),
|
||||
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES)),
|
||||
|
||||
ADMIN(
|
||||
listOf(
|
||||
REMOVE,
|
||||
SET_BROWSER_DEFAULT_GROUP,
|
||||
EDIT_CATALOG,
|
||||
|
||||
// Admin only permissions
|
||||
REMOVE_EMPLOYEE,
|
||||
EDIT_EMPLOYEE_PASSWORD,
|
||||
REMOVE_EMPLOYEE_GROUP,
|
||||
REMOVE_RECIPES,
|
||||
REMOVE_USERS,
|
||||
REMOVE_CATALOG,
|
||||
|
||||
PRINT_MIXES,
|
||||
ADD_TO_INVENTORY,
|
||||
DEDUCT_FROM_INVENTORY
|
||||
)
|
||||
);
|
||||
),
|
||||
|
||||
// deprecated permissions
|
||||
VIEW_RECIPE(listOf(VIEW_RECIPES), true),
|
||||
VIEW_MATERIAL(listOf(VIEW_CATALOG), true),
|
||||
VIEW_MATERIAL_TYPE(listOf(VIEW_CATALOG), true),
|
||||
VIEW_COMPANY(listOf(VIEW_CATALOG), true),
|
||||
VIEW(listOf(VIEW_RECIPES, VIEW_CATALOG), true),
|
||||
VIEW_EMPLOYEE(listOf(VIEW_USERS), true),
|
||||
VIEW_EMPLOYEE_GROUP(listOf(VIEW_USERS), true),
|
||||
|
||||
EDIT_RECIPE(listOf(EDIT_RECIPES), true),
|
||||
EDIT_MATERIAL(listOf(EDIT_MATERIALS), true),
|
||||
EDIT_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPES), true),
|
||||
EDIT_COMPANY(listOf(EDIT_COMPANIES), true),
|
||||
EDIT(listOf(EDIT_RECIPES, EDIT_CATALOG), true),
|
||||
EDIT_EMPLOYEE(listOf(EDIT_USERS), true),
|
||||
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true),
|
||||
EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true),
|
||||
|
||||
REMOVE_RECIPE(listOf(REMOVE_RECIPES), true),
|
||||
REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true),
|
||||
REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true),
|
||||
REMOVE_COMPANY(listOf(REMOVE_COMPANIES), true),
|
||||
REMOVE(listOf(REMOVE_RECIPES, REMOVE_CATALOG), true),
|
||||
REMOVE_EMPLOYEE(listOf(REMOVE_USERS), true),
|
||||
REMOVE_EMPLOYEE_GROUP(listOf(REMOVE_USERS), true),
|
||||
|
||||
SET_BROWSER_DEFAULT_GROUP(listOf(VIEW_USERS), true),
|
||||
;
|
||||
|
||||
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() })
|
||||
grantedPermissions.addAll(permissions.flatMap { it.flat() })
|
||||
return grantedPermissions
|
||||
}
|
||||
|
||||
private fun EmployeePermission.flat(): Iterable<EmployeePermission> {
|
||||
fun EmployeePermission.flat(): Iterable<EmployeePermission> {
|
||||
return mutableSetOf(this).apply {
|
||||
impliedPermissions.forEach {
|
||||
addAll(it.flat())
|
||||
|
@ -311,9 +305,10 @@ fun employeeUpdateDto(
|
|||
id: Long = 0L,
|
||||
firstName: String = "firstName",
|
||||
lastName: String = "lastName",
|
||||
groupId: Long? = null,
|
||||
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
|
||||
op: EmployeeUpdateDto.() -> Unit = {}
|
||||
) = EmployeeUpdateDto(id, firstName, lastName, permissions).apply(op)
|
||||
) = EmployeeUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
|
||||
|
||||
fun employeeGroup(
|
||||
id: Long? = null,
|
||||
|
|
|
@ -1,123 +1,151 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveUsers
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.service.EmployeeGroupServiceImpl
|
||||
import dev.fyloz.colorrecipesexplorer.service.EmployeeServiceImpl
|
||||
import org.springframework.http.HttpStatus
|
||||
import dev.fyloz.colorrecipesexplorer.service.EmployeeService
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.security.Principal
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.validation.Valid
|
||||
|
||||
private const val EMPLOYEE_CONTROLLER_PATH = "api/employee"
|
||||
private const val EMPLOYEE_GROUP_CONTROLLER_PATH = "api/employee/group"
|
||||
|
||||
@RestController
|
||||
@RequestMapping(EMPLOYEE_CONTROLLER_PATH)
|
||||
class EmployeeController(employeeService: EmployeeServiceImpl) :
|
||||
AbstractModelRestApiController<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeServiceImpl>(
|
||||
employeeService,
|
||||
EMPLOYEE_CONTROLLER_PATH
|
||||
) {
|
||||
class EmployeeController(private val employeeService: EmployeeService) {
|
||||
@GetMapping
|
||||
@PreAuthorizeViewUsers
|
||||
fun getAll() =
|
||||
ok(employeeService.getAll())
|
||||
|
||||
@GetMapping("{id}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(employeeService.getById(id))
|
||||
|
||||
@GetMapping("current")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getCurrent(loggedInEmployee: Principal?): ResponseEntity<Employee> = if (loggedInEmployee != null)
|
||||
ResponseEntity.ok(
|
||||
service.getById(
|
||||
loggedInEmployee.name.toLong(),
|
||||
ignoreDefaultGroupUsers = false,
|
||||
ignoreSystemUsers = false
|
||||
fun getCurrent(loggedInEmployee: Principal?) =
|
||||
if (loggedInEmployee != null)
|
||||
ok(
|
||||
employeeService.getById(
|
||||
loggedInEmployee.name.toLong(),
|
||||
ignoreDefaultGroupUsers = false,
|
||||
ignoreSystemUsers = false
|
||||
)
|
||||
)
|
||||
)
|
||||
else
|
||||
ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
else
|
||||
forbidden()
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun save(@Valid @RequestBody employee: EmployeeSaveDto) =
|
||||
created<Employee>(EMPLOYEE_CONTROLLER_PATH) {
|
||||
employeeService.save(employee)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun update(@Valid @RequestBody employee: EmployeeUpdateDto) =
|
||||
noContent {
|
||||
employeeService.update(employee)
|
||||
}
|
||||
|
||||
@PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE])
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun updatePassword(@PathVariable id: Long, @RequestBody password: String): ResponseEntity<Void> {
|
||||
service.updatePassword(id, password)
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build()
|
||||
}
|
||||
@PreAuthorizeEditUsers
|
||||
fun updatePassword(@PathVariable id: Long, @RequestBody password: String) =
|
||||
noContent {
|
||||
employeeService.updatePassword(id, password)
|
||||
}
|
||||
|
||||
@PutMapping("{employeeId}/permissions/{permission}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorizeEditUsers
|
||||
fun addPermission(
|
||||
@PathVariable employeeId: Long,
|
||||
@PathVariable permission: EmployeePermission
|
||||
): ResponseEntity<Void> {
|
||||
service.addPermission(employeeId, permission)
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build()
|
||||
) = noContent {
|
||||
employeeService.addPermission(employeeId, permission)
|
||||
}
|
||||
|
||||
@DeleteMapping("{employeeId}/permissions/{permission}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorizeEditUsers
|
||||
fun removePermission(
|
||||
@PathVariable employeeId: Long,
|
||||
@PathVariable permission: EmployeePermission
|
||||
): ResponseEntity<Void> {
|
||||
service.removePermission(employeeId, permission)
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build()
|
||||
) = noContent {
|
||||
employeeService.removePermission(employeeId, permission)
|
||||
}
|
||||
|
||||
@GetMapping("logout")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun logout(request: HttpServletRequest): ResponseEntity<Void> {
|
||||
service.logout(request)
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeRemoveUsers
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
employeeService.deleteById(id)
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping(EMPLOYEE_GROUP_CONTROLLER_PATH)
|
||||
class GroupsController(groupService: EmployeeGroupServiceImpl) :
|
||||
AbstractModelRestApiController<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupServiceImpl>(
|
||||
groupService,
|
||||
EMPLOYEE_GROUP_CONTROLLER_PATH
|
||||
) {
|
||||
class GroupsController(private val groupService: EmployeeGroupServiceImpl) {
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
|
||||
fun getAll() =
|
||||
ok(groupService.getAll())
|
||||
|
||||
@GetMapping("{id}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(groupService.getById(id))
|
||||
|
||||
@GetMapping("{id}/employees")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getEmployeesForGroup(@PathVariable id: Long): ResponseEntity<Collection<Employee>> =
|
||||
ResponseEntity.ok(service.getEmployeesForGroup(id))
|
||||
@PreAuthorizeViewUsers
|
||||
fun getEmployeesForGroup(@PathVariable id: Long) =
|
||||
ok(groupService.getEmployeesForGroup(id))
|
||||
|
||||
@PostMapping("default/{groupId}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse): ResponseEntity<Void> {
|
||||
service.setResponseDefaultGroup(groupId, response)
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build()
|
||||
}
|
||||
@PreAuthorizeViewUsers
|
||||
fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse) =
|
||||
noContent {
|
||||
groupService.setResponseDefaultGroup(groupId, response)
|
||||
}
|
||||
|
||||
@GetMapping("default")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest): ResponseEntity<EmployeeGroup> =
|
||||
ResponseEntity.ok(service.getRequestDefaultGroup(request))
|
||||
@PreAuthorizeViewUsers
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest) =
|
||||
ok(groupService.getRequestDefaultGroup(request))
|
||||
|
||||
@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()
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() // TODO Go to employee controller
|
||||
}
|
||||
@PostMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun save(@Valid @RequestBody group: EmployeeGroupSaveDto) =
|
||||
created<EmployeeGroup>(EMPLOYEE_GROUP_CONTROLLER_PATH) {
|
||||
groupService.save(group)
|
||||
}
|
||||
|
||||
@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()
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() // TODO Go to employee controller
|
||||
}
|
||||
@PutMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun update(@Valid @RequestBody group: EmployeeGroupUpdateDto) =
|
||||
noContent {
|
||||
groupService.update(group)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeRemoveUsers
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
groupService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api")
|
||||
class LogoutController(private val employeeService: EmployeeService) {
|
||||
@GetMapping("logout")
|
||||
fun logout(request: HttpServletRequest) =
|
||||
ok<Void> {
|
||||
employeeService.logout(request)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,46 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog
|
||||
import dev.fyloz.colorrecipesexplorer.model.Company
|
||||
import dev.fyloz.colorrecipesexplorer.model.CompanySaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.CompanyUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.service.CompanyService
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import javax.validation.Valid
|
||||
|
||||
private const val COMPANY_CONTROLLER_PATH = "api/company"
|
||||
|
||||
@RestController
|
||||
@RequestMapping(COMPANY_CONTROLLER_PATH)
|
||||
class CompanyController(companyService: CompanyService) :
|
||||
AbstractModelRestApiController<Company, CompanySaveDto, CompanyUpdateDto, CompanyService>(
|
||||
companyService,
|
||||
COMPANY_CONTROLLER_PATH
|
||||
)
|
||||
@PreAuthorizeViewCatalog
|
||||
class CompanyController(private val companyService: CompanyService) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(companyService.getAll())
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(companyService.getById(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
|
||||
fun save(@Valid @RequestBody company: CompanySaveDto) =
|
||||
created<Company>(COMPANY_CONTROLLER_PATH) {
|
||||
companyService.save(company)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
|
||||
fun update(@Valid @RequestBody company: CompanyUpdateDto) =
|
||||
noContent {
|
||||
companyService.update(company)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorize("hasAuthority('REMOVE_COMPANIES')")
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
companyService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto
|
|||
import dev.fyloz.colorrecipesexplorer.model.MixDeductDto
|
||||
import dev.fyloz.colorrecipesexplorer.service.InventoryService
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
|
@ -17,17 +18,18 @@ class InventoryController(
|
|||
private val inventoryService: InventoryService
|
||||
) {
|
||||
@PutMapping("add")
|
||||
@PreAuthorize("hasAuthority('ADD_TO_INVENTORY')")
|
||||
fun add(@RequestBody quantities: Collection<MaterialQuantityDto>): ResponseEntity<Collection<MaterialQuantityDto>> {
|
||||
return ResponseEntity.ok(inventoryService.add(quantities))
|
||||
}
|
||||
|
||||
@PutMapping("deduct")
|
||||
fun deduct(@RequestBody quantities: Collection<MaterialQuantityDto>): ResponseEntity<Collection<MaterialQuantityDto>> {
|
||||
return ResponseEntity.ok(inventoryService.deduct(quantities))
|
||||
}
|
||||
@PreAuthorize("hasAuthority('DEDUCT_FROM_INVENTORY')")
|
||||
fun deduct(@RequestBody quantities: Collection<MaterialQuantityDto>) =
|
||||
ok(inventoryService.deduct(quantities))
|
||||
|
||||
@PutMapping("deduct/mix")
|
||||
fun deduct(@RequestBody mixRatio: MixDeductDto): ResponseEntity<Collection<MaterialQuantityDto>> {
|
||||
return ResponseEntity.ok(inventoryService.deductMix(mixRatio))
|
||||
}
|
||||
@PreAuthorize("hasAuthority('DEDUCT_FROM_INVENTORY')")
|
||||
fun deduct(@RequestBody mixRatio: MixDeductDto) =
|
||||
ok(inventoryService.deductMix(mixRatio))
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.service.MaterialService
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import javax.validation.Valid
|
||||
|
@ -14,75 +14,79 @@ private const val MATERIAL_CONTROLLER_PATH = "api/material"
|
|||
|
||||
@RestController
|
||||
@RequestMapping(MATERIAL_CONTROLLER_PATH)
|
||||
class MaterialController(materialService: MaterialService) :
|
||||
AbstractModelRestApiController<Material, MaterialSaveDto, MaterialUpdateDto, MaterialService>(
|
||||
materialService,
|
||||
MATERIAL_CONTROLLER_PATH
|
||||
) {
|
||||
@PreAuthorizeViewCatalog
|
||||
class MaterialController(private val materialService: MaterialService) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(materialService.getAll())
|
||||
|
||||
@GetMapping("notmixtype")
|
||||
fun getAllNotMixType(): ResponseEntity<Collection<Material>> =
|
||||
ResponseEntity.ok(service.getAllNotMixType())
|
||||
fun getAllNotMixType() =
|
||||
ok(materialService.getAllNotMixType())
|
||||
|
||||
@GetMapping("mix/create/{recipeId}")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getAllForMixCreation(@PathVariable recipeId: Long): ResponseEntity<Collection<Material>> =
|
||||
ResponseEntity.ok(service.getAllForMixCreation(recipeId))
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(materialService.getById(id))
|
||||
|
||||
@GetMapping("mix/update/{mixId}")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getAllForMixUpdate(@PathVariable mixId: Long): ResponseEntity<Collection<Material>> =
|
||||
ResponseEntity.ok(service.getAllForMixUpdate(mixId))
|
||||
@PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@PreAuthorize("hasAuthority('EDIT_MATERIALS')")
|
||||
fun save(@Valid @RequestBody material: MaterialSaveDto, simdutFile: MultipartFile?) =
|
||||
created<Material>(MATERIAL_CONTROLLER_PATH) {
|
||||
materialService.save(
|
||||
materialSaveDto(
|
||||
name = material.name,
|
||||
inventoryQuantity = material.inventoryQuantity,
|
||||
materialTypeId = material.materialTypeId,
|
||||
simdutFile = simdutFile
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@PreAuthorize("hasAuthority('EDIT_MATERIALS')")
|
||||
fun update(@Valid @RequestBody material: MaterialUpdateDto, simdutFile: MultipartFile?) =
|
||||
noContent {
|
||||
materialService.update(
|
||||
materialUpdateDto(
|
||||
id = material.id,
|
||||
name = material.name,
|
||||
inventoryQuantity = material.inventoryQuantity,
|
||||
materialTypeId = material.materialTypeId,
|
||||
simdutFile = simdutFile
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorize("hasAuthority('REMOVE_MATERIALS')")
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
materialService.deleteById(id)
|
||||
}
|
||||
|
||||
@GetMapping("{id}/simdut/exists")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun hasSimdut(@PathVariable id: Long): ResponseEntity<Boolean> =
|
||||
ResponseEntity.ok(service.hasSimdut(id))
|
||||
fun hasSimdut(@PathVariable id: Long) =
|
||||
ok(materialService.hasSimdut(id))
|
||||
|
||||
@GetMapping("{id}/simdut", produces = [MediaType.APPLICATION_PDF_VALUE])
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getSimdut(@PathVariable id: Long): ResponseEntity<ByteArray> {
|
||||
val simdutFile = service.getSimdut(id)
|
||||
return if (simdutFile.isEmpty()) {
|
||||
ResponseEntity.notFound().build()
|
||||
fun getSimdut(@PathVariable id: Long): ResponseEntity<ByteArray> = with(materialService.getSimdut(id)) {
|
||||
if (this.isEmpty()) {
|
||||
notFound()
|
||||
} else {
|
||||
val headers = HttpHeaders().apply { contentType = MediaType.APPLICATION_PDF }
|
||||
ResponseEntity(simdutFile, headers, HttpStatus.OK)
|
||||
ok(this, httpHeaders(contentType = MediaType.APPLICATION_PDF))
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/simdut")
|
||||
fun getAllIdsWithSimdut(): ResponseEntity<Collection<Long>> =
|
||||
ResponseEntity.ok(service.getAllIdsWithSimdut())
|
||||
fun getAllIdsWithSimdut() =
|
||||
ok(materialService.getAllIdsWithSimdut())
|
||||
|
||||
@PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
fun save(@Valid entity: MaterialSaveDto, simdutFile: MultipartFile?): ResponseEntity<Material> =
|
||||
super.save(
|
||||
materialSaveDto(
|
||||
name = entity.name,
|
||||
inventoryQuantity = entity.inventoryQuantity,
|
||||
materialTypeId = entity.materialTypeId,
|
||||
simdutFile = simdutFile
|
||||
)
|
||||
)
|
||||
@GetMapping("mix/create/{recipeId}")
|
||||
fun getAllForMixCreation(@PathVariable recipeId: Long) =
|
||||
ok(materialService.getAllForMixCreation(recipeId))
|
||||
|
||||
@PostMapping("unused_save")
|
||||
override fun save(entity: MaterialSaveDto): ResponseEntity<Material> =
|
||||
ResponseEntity.notFound().build()
|
||||
|
||||
@PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
fun update(@Valid entity: MaterialUpdateDto, simdutFile: MultipartFile?): ResponseEntity<Void> =
|
||||
super.update(
|
||||
materialUpdateDto(
|
||||
id = entity.id,
|
||||
name = entity.name,
|
||||
inventoryQuantity = entity.inventoryQuantity,
|
||||
materialTypeId = entity.materialTypeId,
|
||||
simdutFile = simdutFile
|
||||
)
|
||||
)
|
||||
|
||||
@PutMapping("unused_update")
|
||||
override fun update(entity: MaterialUpdateDto): ResponseEntity<Void> =
|
||||
ResponseEntity.notFound().build()
|
||||
@GetMapping("mix/update/{mixId}")
|
||||
fun getAllForMixUpdate(@PathVariable mixId: Long) =
|
||||
ok(materialService.getAllForMixUpdate(mixId))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,47 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog
|
||||
import dev.fyloz.colorrecipesexplorer.model.MaterialType
|
||||
import dev.fyloz.colorrecipesexplorer.model.MaterialTypeSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.MaterialTypeUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import javax.validation.Valid
|
||||
|
||||
private const val MATERIAL_TYPE_CONTROLLER_PATH = "api/materialtype"
|
||||
|
||||
@RestController
|
||||
@RequestMapping(MATERIAL_TYPE_CONTROLLER_PATH)
|
||||
class MaterialTypeController(materialTypeService: MaterialTypeService) :
|
||||
AbstractModelRestApiController<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeService>(
|
||||
materialTypeService,
|
||||
MATERIAL_TYPE_CONTROLLER_PATH
|
||||
)
|
||||
@PreAuthorizeViewCatalog
|
||||
class MaterialTypeController(private val materialTypeService: MaterialTypeService) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(materialTypeService.getAll())
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(materialTypeService.getById(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')")
|
||||
fun save(@Valid @RequestBody materialType: MaterialTypeSaveDto) =
|
||||
created<MaterialType>(MATERIAL_TYPE_CONTROLLER_PATH) {
|
||||
materialTypeService.save(materialType)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')")
|
||||
fun update(@Valid @RequestBody materialType: MaterialTypeUpdateDto) =
|
||||
noContent {
|
||||
materialTypeService.update(materialType)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorize("hasAuthority('REMOVE_MATERIAL_TYPEs')")
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
materialTypeService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveRecipes
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.service.MixService
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeImageService
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeService
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.net.URI
|
||||
|
@ -18,49 +21,98 @@ private const val MIX_CONTROLLER_PATH = "api/recipe/mix"
|
|||
|
||||
@RestController
|
||||
@RequestMapping(RECIPE_CONTROLLER_PATH)
|
||||
class RecipeController(recipeService: RecipeService) :
|
||||
AbstractModelRestApiController<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService>(
|
||||
recipeService,
|
||||
RECIPE_CONTROLLER_PATH
|
||||
) {
|
||||
@PreAuthorizeViewRecipes
|
||||
class RecipeController(private val recipeService: RecipeService) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(recipeService.getAll())
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(recipeService.getById(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun save(@Valid @RequestBody recipe: RecipeSaveDto) =
|
||||
created<Recipe>(RECIPE_CONTROLLER_PATH) {
|
||||
recipeService.save(recipe)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun update(@Valid @RequestBody recipe: RecipeUpdateDto) =
|
||||
noContent {
|
||||
recipeService.update(recipe)
|
||||
}
|
||||
|
||||
@PutMapping("public")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto): ResponseEntity<Void> {
|
||||
service.updatePublicData(publicDataDto)
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
@PreAuthorize("hasAuthority('EDIT_RECIPES_PUBLIC_DATA')")
|
||||
fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) =
|
||||
noContent {
|
||||
recipeService.updatePublicData(publicDataDto)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeRemoveRecipes
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
recipeService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping(RECIPE_CONTROLLER_PATH)
|
||||
@PreAuthorizeViewRecipes
|
||||
class RecipeImageController(val recipeImageService: RecipeImageService) {
|
||||
@GetMapping("{recipeId}/image")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getAllIdsForRecipe(@PathVariable recipeId: Long): ResponseEntity<Collection<Long>> =
|
||||
ResponseEntity.ok(recipeImageService.getAllIdsForRecipe(recipeId))
|
||||
fun getAllIdsForRecipe(@PathVariable recipeId: Long) =
|
||||
ok(recipeImageService.getAllIdsForRecipe(recipeId))
|
||||
|
||||
@GetMapping("{recipeId}/image/{id}", produces = [MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE])
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getById(@PathVariable recipeId: Long, @PathVariable id: Long): ResponseEntity<ByteArray> =
|
||||
ResponseEntity.ok(recipeImageService.getByIdForRecipe(id, recipeId))
|
||||
fun getById(@PathVariable recipeId: Long, @PathVariable id: Long) =
|
||||
ok(recipeImageService.getByIdForRecipe(id, recipeId))
|
||||
|
||||
@PostMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@PreAuthorizeEditRecipes
|
||||
fun save(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity<Void> {
|
||||
val id = recipeImageService.save(image, recipeId)
|
||||
return ResponseEntity.created(URI.create("/$RECIPE_CONTROLLER_PATH/$recipeId/image/$id")).build()
|
||||
}
|
||||
|
||||
@DeleteMapping("{recipeId}/image/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun delete(@PathVariable recipeId: Long, @PathVariable id: Long): ResponseEntity<Void> {
|
||||
recipeImageService.delete(id, recipeId)
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
@PreAuthorizeRemoveRecipes
|
||||
fun delete(@PathVariable recipeId: Long, @PathVariable id: Long) =
|
||||
noContent {
|
||||
recipeImageService.delete(id, recipeId)
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping(MIX_CONTROLLER_PATH)
|
||||
class MixController(val mixService: MixService) :
|
||||
AbstractModelRestApiController<Mix, MixSaveDto, MixUpdateDto, MixService>(mixService, MIX_CONTROLLER_PATH)
|
||||
@PreAuthorizeViewRecipes
|
||||
class MixController(private val mixService: MixService) {
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(mixService.getById(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun save(@Valid @RequestBody mix: MixSaveDto) =
|
||||
created<Mix>(MIX_CONTROLLER_PATH) {
|
||||
mixService.save(mix)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun update(@Valid @RequestBody mix: MixUpdateDto) =
|
||||
noContent {
|
||||
mixService.update(mix)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeRemoveRecipes
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
mixService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import dev.fyloz.colorrecipesexplorer.service.ExternalModelService
|
||||
import dev.fyloz.colorrecipesexplorer.service.ExternalService
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.net.URI
|
||||
import javax.validation.Valid
|
||||
|
||||
interface RestApiController<E, S : EntityDto<E>, U : EntityDto<E>> {
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getAll(): ResponseEntity<Iterable<E>>
|
||||
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun save(entity: S): ResponseEntity<E>
|
||||
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun update(entity: U): ResponseEntity<Void>
|
||||
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun delete(entity: E): ResponseEntity<Void>
|
||||
}
|
||||
|
||||
interface RestModelApiController<E : Model, S : EntityDto<E>, U : EntityDto<E>> : RestApiController<E, S, U> {
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
fun getById(id: Long): ResponseEntity<E>
|
||||
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun deleteById(id: Long): ResponseEntity<Void>
|
||||
}
|
||||
|
||||
abstract class AbstractRestApiController<E, N : EntityDto<E>, U : EntityDto<E>, S : ExternalService<E, N, U, *>>(
|
||||
val service: S,
|
||||
private val controllerPath: String
|
||||
) :
|
||||
RestApiController<E, N, U> {
|
||||
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: N): ResponseEntity<E> {
|
||||
val saved = service.save(entity)
|
||||
return ResponseEntity
|
||||
.created(URI("$controllerPath/${getEntityId(saved)}"))
|
||||
.body(saved)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
override fun update(@Valid @RequestBody entity: U): 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 AbstractModelRestApiController<E : Model, N : EntityDto<E>, U : EntityDto<E>, S : ExternalModelService<E, N, U, *>>(
|
||||
service: S,
|
||||
controllerPath: String
|
||||
) :
|
||||
AbstractRestApiController<E, N, U, S>(service, controllerPath), RestModelApiController<E, N, U> {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import java.net.URI
|
||||
|
||||
/** Creates a HTTP OK [ResponseEntity] from the given [body]. */
|
||||
fun <T> ok(body: T): ResponseEntity<T> =
|
||||
ResponseEntity.ok(body)
|
||||
|
||||
/** Creates a HTTP OK [ResponseEntity] from the given [body] and [headers]. */
|
||||
fun <T> ok(body: T, headers: HttpHeaders): ResponseEntity<T> =
|
||||
ResponseEntity(body, headers, HttpStatus.OK)
|
||||
|
||||
/** Executes the given [action] then returns an HTTP OK [ResponseEntity] form the given [body]. */
|
||||
fun <T> ok(action: () -> Unit): ResponseEntity<T> {
|
||||
action()
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
|
||||
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
|
||||
fun <T : Model> created(controllerPath: String, body: T): ResponseEntity<T> =
|
||||
ResponseEntity.created(URI.create("$controllerPath/${body.id}")).body(body)
|
||||
|
||||
/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
|
||||
fun <T : Model> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
|
||||
created(controllerPath, producer())
|
||||
|
||||
/** Creates a HTTP NOT FOUND [ResponseEntity]. */
|
||||
fun <T> notFound(): ResponseEntity<T> =
|
||||
ResponseEntity.notFound().build()
|
||||
|
||||
/** Creates a HTTP NO CONTENT [ResponseEntity]. */
|
||||
fun noContent(): ResponseEntity<Void> =
|
||||
ResponseEntity.noContent().build()
|
||||
|
||||
/** Executes the given [action] then returns an HTTP NO CONTENT [ResponseEntity]. */
|
||||
fun noContent(action: () -> Unit): ResponseEntity<Void> {
|
||||
action()
|
||||
return noContent()
|
||||
}
|
||||
|
||||
/** Creates a HTTP FORBIDDEN [ResponseEntity]. */
|
||||
fun <T> forbidden(): ResponseEntity<T> =
|
||||
ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
|
||||
/** Creates an [HttpHeaders] instance from the given options. */
|
||||
fun httpHeaders(
|
||||
contentType: MediaType = MediaType.APPLICATION_JSON,
|
||||
op: HttpHeaders.() -> Unit = {}
|
||||
) = HttpHeaders().apply {
|
||||
this.contentType = contentType
|
||||
|
||||
op()
|
||||
}
|
|
@ -116,7 +116,7 @@ class EmployeeServiceImpl(
|
|||
passwordEncoder.encode(password),
|
||||
isDefaultGroupUser = false,
|
||||
isSystemUser = false,
|
||||
group = if (groupId != null) groupService.getById(groupId!!) else null,
|
||||
group = if (groupId != null) groupService.getById(groupId) else null,
|
||||
permissions = permissions
|
||||
)
|
||||
})
|
||||
|
@ -160,7 +160,7 @@ class EmployeeServiceImpl(
|
|||
password = persistedEmployee.password,
|
||||
isDefaultGroupUser = false,
|
||||
isSystemUser = false,
|
||||
group = persistedEmployee.group,
|
||||
group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedEmployee.group,
|
||||
permissions = permissions?.toMutableSet() ?: persistedEmployee.permissions,
|
||||
lastLoginTime = persistedEmployee.lastLoginTime
|
||||
)
|
||||
|
@ -294,6 +294,6 @@ class EmployeeUserDetailsServiceImpl(
|
|||
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
|
||||
ignoreSystemUsers = false
|
||||
)
|
||||
return User(employee.id.toString(), employee.password, employee.getAuthorities())
|
||||
return User(employee.id.toString(), employee.password, employee.authorities)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ class RecipeServiceImpl(
|
|||
return updatedGroupsInformation
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun updatePublicData(publicDataDto: RecipePublicDataDto) {
|
||||
if (publicDataDto.notes != null) {
|
||||
val recipe = getById(publicDataDto.recipeId)
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import dev.fyloz.colorrecipesexplorer.model.NamedModel
|
||||
import dev.fyloz.colorrecipesexplorer.repository.NamedJpaRepository
|
||||
import dev.fyloz.colorrecipesexplorer.rest.RestApiController
|
||||
import io.jsonwebtoken.lang.Assert
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
|
@ -114,13 +112,13 @@ abstract class AbstractNamedModelService<E : NamedModel, R : NamedJpaRepository<
|
|||
return super.update(entity)
|
||||
}
|
||||
|
||||
protected fun assertName(name: String) {
|
||||
private fun assertName(name: String) {
|
||||
Assert.notNull(name, "${javaClass.simpleName}.update() was called with a null name")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A service that will receive *external* interactions, from a [RestApiController] for example.
|
||||
* A service that will receive *external* interactions, from the REST API, for example.
|
||||
*
|
||||
* @param E The entity type
|
||||
* @param S The entity save DTO type
|
||||
|
@ -156,8 +154,3 @@ abstract class AbstractExternalModelService<E : Model, S : EntityDto<E>, U : Ent
|
|||
abstract class AbstractExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, R : NamedJpaRepository<E>>(
|
||||
repository: R
|
||||
) : AbstractNamedModelService<E, R>(repository), ExternalNamedModelService<E, S, U, R>
|
||||
|
||||
/** Transforms the given object to JSON. **/
|
||||
fun Any.asJson(): String {
|
||||
return jacksonObjectMapper().writeValueAsString(this)
|
||||
}
|
||||
|
|
|
@ -163,7 +163,6 @@ class EmployeeServiceTest :
|
|||
|
||||
@Test
|
||||
fun `save(dto) calls and returns save() with the created employee`() {
|
||||
whenever(entitySaveDto.toEntity()).doReturn(entitySaveDtoEmployee)
|
||||
doReturn(entitySaveDtoEmployee).whenever(service).save(any<Employee>())
|
||||
|
||||
val found = service.save(entitySaveDto)
|
||||
|
|
Loading…
Reference in New Issue