Amélioration de l'implémentation des controlleurs.

This commit is contained in:
FyloZ 2021-04-04 21:02:53 -04:00
parent ac4be0b330
commit 1bd0c94a4d
13 changed files with 388 additions and 407 deletions

View File

@ -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,9 @@ 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()
.anyRequest().authenticated()
} else {
http
.cors()
@ -304,83 +284,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
)
)
}

View File

@ -80,25 +80,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 +96,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>
@ -311,9 +299,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,

View File

@ -2,122 +2,144 @@ package dev.fyloz.colorrecipesexplorer.rest
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
) {
@PreAuthorize("hasAuthority('VIEW_EMPLOYEE')")
class EmployeeController(private val employeeService: EmployeeService) {
@GetMapping
fun getAll() =
ok(employeeService.getAll())
@GetMapping("{id}")
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
@PreAuthorize("authenticated()")
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
@PreAuthorize("hasAuthority('EDIT_EMPLOYEE')")
fun save(@Valid @RequestBody employee: EmployeeSaveDto) =
created<Employee>(EMPLOYEE_CONTROLLER_PATH) {
employeeService.save(employee)
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_EMPLOYEE')")
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()
}
@PreAuthorize("hasAuthority('EDIT_EMPLOYEE_PASSWORD')")
fun updatePassword(@PathVariable id: Long, @RequestBody password: String) =
noContent {
employeeService.updatePassword(id, password)
}
@PutMapping("{employeeId}/permissions/{permission}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasAuthority('EDIT_EMPLOYEE')")
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)
@PreAuthorize("hasAuthority('EDIT_EMPLOYEE')")
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}")
@PreAuthorize("hasAuthority('REMOVE_EMPLOYEE')")
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
) {
@PreAuthorize("hasAuthority('VIEW_EMPLOYEE')")
class GroupsController(private val groupService: EmployeeGroupServiceImpl) {
@GetMapping
fun getAll() =
ok(groupService.getAll())
@GetMapping("{id}")
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))
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()
}
@PreAuthorize("hasAuthority('SET_BROWSER_DEFAULT_GROUP')")
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))
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
@PreAuthorize("hasAuthority('EDIT_EMPLOYEE')")
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
@PreAuthorize("hasAuthority('EDIT_EMPLOYEE')")
fun update(@Valid @RequestBody group: EmployeeGroupUpdateDto) =
noContent {
groupService.update(group)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_EMPLOYEE')")
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)
}
}

View File

@ -4,15 +4,42 @@ 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
)
@PreAuthorize("hasAuthority('VIEW_COMPANY')")
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_COMPANY')")
fun save(@Valid @RequestBody company: CompanySaveDto) =
created<Company>(COMPANY_CONTROLLER_PATH) {
companyService.save(company)
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_COMPANY')")
fun update(@Valid @RequestBody company: CompanyUpdateDto) =
noContent {
companyService.update(company)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_COMPANY')")
fun deleteById(@PathVariable id: Long) =
noContent {
companyService.deleteById(id)
}
}

View File

@ -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('EDIT_MATERIAL')")
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('VIEW_RECIPE')")
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('VIEW_RECIPE')")
fun deduct(@RequestBody mixRatio: MixDeductDto) =
ok(inventoryService.deductMix(mixRatio))
}

View File

@ -2,10 +2,9 @@ package dev.fyloz.colorrecipesexplorer.rest
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 +13,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
) {
@PreAuthorize("hasAuthority('VIEW_MATERIAL')")
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_MATERIAL')")
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_MATERIAL')")
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_MATERIAL')")
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))
}

View File

@ -4,16 +4,43 @@ 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
)
@PreAuthorize("hasAuthority('VIEW_MATERIAL_TYPE')")
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_TYPE')")
fun save(@Valid @RequestBody materialType: MaterialTypeSaveDto) =
created<MaterialType>(MATERIAL_TYPE_CONTROLLER_PATH) {
materialTypeService.save(materialType)
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPE')")
fun update(@Valid @RequestBody materialType: MaterialTypeUpdateDto) =
noContent {
materialTypeService.update(materialType)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_MATERIAL_TYPE')")
fun deleteById(@PathVariable id: Long) =
noContent {
materialTypeService.deleteById(id)
}
}

View File

@ -4,9 +4,9 @@ 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 +18,97 @@ 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
) {
@PreAuthorize("hasAuthority('VIEW_RECIPE')")
class RecipeController(private val recipeService: RecipeService) {
@GetMapping
fun getAll() =
ok(recipeService.getAll())
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(recipeService.getById(id))
@PostMapping
@PreAuthorize("hasAuthority('EDIT_RECIPE')")
fun save(@Valid @RequestBody recipe: RecipeSaveDto) =
created<Recipe>(RECIPE_CONTROLLER_PATH) {
recipeService.save(recipe)
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_RECIPE')")
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()
}
fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) =
noContent {
recipeService.updatePublicData(publicDataDto)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_RECIPE')")
fun deleteById(@PathVariable id: Long) =
noContent {
recipeService.deleteById(id)
}
}
@RestController
@RequestMapping(RECIPE_CONTROLLER_PATH)
@PreAuthorize("hasAuthority('VIEW_RECIPE')")
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)
@PreAuthorize("hasAuthority('EDIT_RECIPE')")
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()
}
@PreAuthorize("hasAuthority('REMOVE_RECIPE')")
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)
@PreAuthorize("hasAuthority('VIEW_RECIPE')")
class MixController(private val mixService: MixService) {
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(mixService.getById(id))
@PostMapping
@PreAuthorize("hasAuthority('EDIT_RECIPE')")
fun save(@Valid @RequestBody mix: MixSaveDto) =
created<Mix>(MIX_CONTROLLER_PATH) {
mixService.save(mix)
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_RECIPE')")
fun update(@Valid @RequestBody mix: MixUpdateDto) =
noContent {
mixService.update(mix)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_RECIPE')")
fun deleteById(@PathVariable id: Long) =
noContent {
mixService.deleteById(id)
}
}

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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
)

View File

@ -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)
}

View File

@ -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)