diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt index 0e793a3..45422f6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt @@ -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.ExpressionInterceptUrlRegistry.generateAuthorizations(): - ExpressionUrlAuthorizationConfigurer.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 -) { - 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 - ) - ) -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/AccountModel.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/AccountModel.kt index d1503bd..bb1bf85 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/AccountModel.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/AccountModel.kt @@ -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 = mutableSetOf() -) : EntityDto { - override fun toEntity(): Employee = - Employee( - id, - firstName, - lastName, - "", - isDefaultGroupUser = false, - isSystemUser = false, - group = null, - permissions = permissions - ) -} +) : EntityDto 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? ) : EntityDto @@ -311,9 +299,10 @@ fun employeeUpdateDto( id: Long = 0L, firstName: String = "firstName", lastName: String = "lastName", + groupId: Long? = null, permissions: MutableSet = mutableSetOf(), op: EmployeeUpdateDto.() -> Unit = {} -) = EmployeeUpdateDto(id, firstName, lastName, permissions).apply(op) +) = EmployeeUpdateDto(id, firstName, lastName, groupId, permissions).apply(op) fun employeeGroup( id: Long? = null, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt index f2d857f..43a718e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/AccountControllers.kt @@ -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( - 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 = 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_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 { - 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 { - 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 { - service.removePermission(employeeId, permission) - return ResponseEntity - .noContent() - .build() + ) = noContent { + employeeService.removePermission(employeeId, permission) } - @GetMapping("logout") - @ResponseStatus(HttpStatus.OK) - fun logout(request: HttpServletRequest): ResponseEntity { - 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( - 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> = - 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 { - 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 = - 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 { -// 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(EMPLOYEE_GROUP_CONTROLLER_PATH) { + groupService.save(group) + } - @DeleteMapping("{groupId}/{employeeId}") - @ResponseStatus(HttpStatus.NO_CONTENT) - fun removeEmployeeFromGroup(@PathVariable groupId: Long, @PathVariable employeeId: Long): ResponseEntity { -// 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 { + employeeService.logout(request) + } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt index 2da0018..d2eb66d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/CompanyController.kt @@ -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( - 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_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) + } +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt index 17b061f..ae9443f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt @@ -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): ResponseEntity> { return ResponseEntity.ok(inventoryService.add(quantities)) } @PutMapping("deduct") - fun deduct(@RequestBody quantities: Collection): ResponseEntity> { - return ResponseEntity.ok(inventoryService.deduct(quantities)) - } + @PreAuthorize("hasAuthority('VIEW_RECIPE')") + fun deduct(@RequestBody quantities: Collection) = + ok(inventoryService.deduct(quantities)) @PutMapping("deduct/mix") - fun deduct(@RequestBody mixRatio: MixDeductDto): ResponseEntity> { - return ResponseEntity.ok(inventoryService.deductMix(mixRatio)) - } + @PreAuthorize("hasAuthority('VIEW_RECIPE')") + fun deduct(@RequestBody mixRatio: MixDeductDto) = + ok(inventoryService.deductMix(mixRatio)) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt index 6f30487..37e7e7b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt @@ -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( - 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> = - ResponseEntity.ok(service.getAllNotMixType()) + fun getAllNotMixType() = + ok(materialService.getAllNotMixType()) - @GetMapping("mix/create/{recipeId}") - @ResponseStatus(HttpStatus.OK) - fun getAllForMixCreation(@PathVariable recipeId: Long): ResponseEntity> = - 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> = - 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_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 = - 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 { - val simdutFile = service.getSimdut(id) - return if (simdutFile.isEmpty()) { - ResponseEntity.notFound().build() + fun getSimdut(@PathVariable id: Long): ResponseEntity = 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> = - ResponseEntity.ok(service.getAllIdsWithSimdut()) + fun getAllIdsWithSimdut() = + ok(materialService.getAllIdsWithSimdut()) - @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) - fun save(@Valid entity: MaterialSaveDto, simdutFile: MultipartFile?): ResponseEntity = - 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 = - ResponseEntity.notFound().build() - - @PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) - fun update(@Valid entity: MaterialUpdateDto, simdutFile: MultipartFile?): ResponseEntity = - 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 = - ResponseEntity.notFound().build() + @GetMapping("mix/update/{mixId}") + fun getAllForMixUpdate(@PathVariable mixId: Long) = + ok(materialService.getAllForMixUpdate(mixId)) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt index 459babe..f17cd4e 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialTypeController.kt @@ -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( - 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(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) + } +} + diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index 5af2e41..c54caa8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -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( - 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_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 { - 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> = - 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 = - 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 { 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 { - 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(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_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) + } +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestApiController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestApiController.kt deleted file mode 100644 index 797794c..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestApiController.kt +++ /dev/null @@ -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, U : EntityDto> { - @ResponseStatus(HttpStatus.OK) - fun getAll(): ResponseEntity> - - @ResponseStatus(HttpStatus.CREATED) - fun save(entity: S): ResponseEntity - - @ResponseStatus(HttpStatus.NO_CONTENT) - fun update(entity: U): ResponseEntity - - @ResponseStatus(HttpStatus.NO_CONTENT) - fun delete(entity: E): ResponseEntity -} - -interface RestModelApiController, U : EntityDto> : RestApiController { - @ResponseStatus(HttpStatus.OK) - fun getById(id: Long): ResponseEntity - - @ResponseStatus(HttpStatus.NO_CONTENT) - fun deleteById(id: Long): ResponseEntity -} - -abstract class AbstractRestApiController, U : EntityDto, S : ExternalService>( - val service: S, - private val controllerPath: String -) : - RestApiController { - protected abstract fun getEntityId(entity: E): Any? - - @GetMapping - override fun getAll(): ResponseEntity> = ResponseEntity.ok(service.getAll()) - - @PostMapping - override fun save(@Valid @RequestBody entity: N): ResponseEntity { - val saved = service.save(entity) - return ResponseEntity - .created(URI("$controllerPath/${getEntityId(saved)}")) - .body(saved) - } - - @PutMapping - override fun update(@Valid @RequestBody entity: U): ResponseEntity { - service.update(entity) - return ResponseEntity - .noContent() - .build() - } - - @DeleteMapping - override fun delete(@Valid @RequestBody entity: E): ResponseEntity { - service.delete(entity) - return ResponseEntity - .noContent() - .build() - } -} - -abstract class AbstractModelRestApiController, U : EntityDto, S : ExternalModelService>( - service: S, - controllerPath: String -) : - AbstractRestApiController(service, controllerPath), RestModelApiController { - override fun getEntityId(entity: E) = entity.id - - @GetMapping("{id}") - override fun getById(@PathVariable id: Long): ResponseEntity = ResponseEntity.ok(service.getById(id)) - - @DeleteMapping("{id}") - override fun deleteById(@PathVariable id: Long): ResponseEntity { - service.deleteById(id) - return ResponseEntity - .noContent() - .build() - } -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt new file mode 100644 index 0000000..0c0de8a --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt @@ -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 ok(body: T): ResponseEntity = + ResponseEntity.ok(body) + +/** Creates a HTTP OK [ResponseEntity] from the given [body] and [headers]. */ +fun ok(body: T, headers: HttpHeaders): ResponseEntity = + ResponseEntity(body, headers, HttpStatus.OK) + +/** Executes the given [action] then returns an HTTP OK [ResponseEntity] form the given [body]. */ +fun ok(action: () -> Unit): ResponseEntity { + action() + return ResponseEntity.ok().build() +} + +/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ +fun created(controllerPath: String, body: T): ResponseEntity = + 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 created(controllerPath: String, producer: () -> T): ResponseEntity = + created(controllerPath, producer()) + +/** Creates a HTTP NOT FOUND [ResponseEntity]. */ +fun notFound(): ResponseEntity = + ResponseEntity.notFound().build() + +/** Creates a HTTP NO CONTENT [ResponseEntity]. */ +fun noContent(): ResponseEntity = + ResponseEntity.noContent().build() + +/** Executes the given [action] then returns an HTTP NO CONTENT [ResponseEntity]. */ +fun noContent(action: () -> Unit): ResponseEntity { + action() + return noContent() +} + +/** Creates a HTTP FORBIDDEN [ResponseEntity]. */ +fun forbidden(): ResponseEntity = + 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() +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt index 214dbbd..65f3ed1 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt @@ -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 ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt index 1b72600..d44f2ff 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt @@ -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, U : Ent abstract class AbstractExternalNamedModelService, U : EntityDto, R : NamedJpaRepository>( repository: R ) : AbstractNamedModelService(repository), ExternalNamedModelService - -/** Transforms the given object to JSON. **/ -fun Any.asJson(): String { - return jacksonObjectMapper().writeValueAsString(this) -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt index 2830f0f..3c7e704 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt @@ -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()) val found = service.save(entitySaveDto)