Skip to content

Commit

Permalink
Merge pull request #145 from NIAEFEUP/feature/roles
Browse files Browse the repository at this point in the history
Role management endpoints
  • Loading branch information
LuisDuarte1 authored Aug 16, 2023
2 parents 793bff1 + 100836c commit 4d457a4
Show file tree
Hide file tree
Showing 17 changed files with 1,234 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package pt.up.fe.ni.website.backend.controller

import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import pt.up.fe.ni.website.backend.dto.auth.UserIdDto
import pt.up.fe.ni.website.backend.dto.entity.role.CreateRoleDto
import pt.up.fe.ni.website.backend.dto.entity.role.UpdateRoleDto
import pt.up.fe.ni.website.backend.dto.roles.PermissionsDto
import pt.up.fe.ni.website.backend.service.RoleService

@RestController
@RequestMapping("/roles")
class RoleController(private val roleService: RoleService) {
@GetMapping
fun getAllRoles() = roleService.getAllRoles()

@GetMapping("/{id}")
fun getRole(@PathVariable id: Long) = roleService.getRoleById(id)

@PostMapping
fun createNewRole(@RequestBody dto: CreateRoleDto) = roleService.createNewRole(dto)

@DeleteMapping("/{id}")
fun deleteRole(@PathVariable id: Long) = roleService.deleteRole(id)

@PutMapping("/{id}")
fun updateRole(@PathVariable id: Long, @RequestBody dto: UpdateRoleDto) = roleService.updateRole(id, dto)

@PostMapping("/{id}/permissions")
fun grantPermissionToRole(
@PathVariable id: Long,
@RequestBody permissionsDto: PermissionsDto
): Map<String, String> {
roleService.grantPermissionToRole(id, permissionsDto.permissions)
return emptyMap()
}

@DeleteMapping("/{id}/permissions")
fun revokePermissionFromRole(
@PathVariable id: Long,
@RequestBody permissionsDto: PermissionsDto
): Map<String, String> {
roleService.revokePermissionFromRole(id, permissionsDto.permissions)
return emptyMap()
}

@PostMapping("/{id}/users")
fun addUserToRole(@PathVariable id: Long, @RequestBody userIdDto: UserIdDto): Map<String, String> {
roleService.addUserToRole(id, userIdDto.userId)
return emptyMap()
}

@DeleteMapping("/{id}/users")
fun removeUserFromRole(@PathVariable id: Long, @RequestBody userIdDto: UserIdDto): Map<String, String> {
roleService.removeUserFromRole(id, userIdDto.userId)
return emptyMap()
}

@PostMapping("/{id}/activities/{activityId}/permissions")
fun addPermissionToPerActivityRole(
@PathVariable id: Long,
@PathVariable activityId: Long,
@RequestBody permissionsDto: PermissionsDto
): Map<String, String> {
roleService.grantPermissionToRoleOnActivity(
id,
activityId,
permissionsDto.permissions
)
return emptyMap()
}

@DeleteMapping("/{id}/activities/{activityId}/permissions")
fun revokePermissionFromPerActivityRole(
@PathVariable id: Long,
@PathVariable activityId: Long,
@RequestBody permissionsDto: PermissionsDto
): Map<String, String> {
roleService.revokePermissionFromRoleOnActivity(
id,
activityId,
permissionsDto.permissions
)
return emptyMap()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package pt.up.fe.ni.website.backend.dto.auth

data class UserIdDto(
val userId: Long
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package pt.up.fe.ni.website.backend.dto.entity

import pt.up.fe.ni.website.backend.dto.entity.role.CreateRoleDto
import pt.up.fe.ni.website.backend.model.Generation

class GenerationDto(
var schoolYear: String?,
val roles: List<RoleDto>
val roles: List<CreateRoleDto>
) : EntityDto<Generation>()
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package pt.up.fe.ni.website.backend.dto.entity
package pt.up.fe.ni.website.backend.dto.entity.role

import com.fasterxml.jackson.annotation.JsonProperty
import pt.up.fe.ni.website.backend.dto.entity.EntityDto
import pt.up.fe.ni.website.backend.dto.entity.PerActivityRoleDto
import pt.up.fe.ni.website.backend.model.Role

class RoleDto(
class CreateRoleDto(
val name: String,
val permissions: List<Int>,

@JsonProperty(required = true)
val isSection: Boolean?,

val accountIds: List<Long> = emptyList(),
val associatedActivities: List<PerActivityRoleDto>
val associatedActivities: List<PerActivityRoleDto>,

val generationId: Long? = null
) : EntityDto<Role>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pt.up.fe.ni.website.backend.dto.entity.role

import pt.up.fe.ni.website.backend.dto.entity.EntityDto
import pt.up.fe.ni.website.backend.model.Role

class UpdateRoleDto(
val name: String,
val isSection: Boolean?
) : EntityDto<Role>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package pt.up.fe.ni.website.backend.dto.roles

import pt.up.fe.ni.website.backend.model.permissions.Permissions

data class PermissionsDto(
val permissions: Permissions
)
2 changes: 0 additions & 2 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Activity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import jakarta.persistence.Inheritance
import jakarta.persistence.InheritanceType
import jakarta.persistence.JoinColumn
import jakarta.persistence.OneToMany
import jakarta.persistence.OrderColumn
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
Expand All @@ -34,7 +33,6 @@ abstract class Activity(
open val teamMembers: MutableList<Account>,

@OneToMany(cascade = [CascadeType.ALL], mappedBy = "activity")
@OrderColumn
@JsonIgnore // TODO: Decide if we want to return perRoles (or IDs) by default
open val associatedRoles: MutableList<@Valid PerActivityRole> = mutableListOf(),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.OneToMany
import jakarta.persistence.OrderColumn
import jakarta.validation.Valid
import pt.up.fe.ni.website.backend.utils.validation.NoDuplicateRoles
import pt.up.fe.ni.website.backend.utils.validation.SchoolYear
Expand All @@ -25,7 +24,6 @@ class Generation(
val id: Long? = null
) {
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, mappedBy = "generation")
@OrderColumn
@JsonManagedReference
@field:NoDuplicateRoles
val roles: MutableList<@Valid Role> = mutableListOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ object ErrorMessages {

const val generationAlreadyExists = "generation already exists"

fun roleAlreadyExists(roleName: String, generationYear: String): String =
"role $roleName already exists in $generationYear"

fun postNotFound(postId: Long): String = "post not found with id $postId"

fun postNotFound(postSlug: String): String = "post not found with slug $postSlug"
Expand All @@ -38,4 +41,10 @@ object ErrorMessages {
fun generationNotFound(year: String): String = "generation not found with year $year"

fun emailNotFound(email: String): String = "account not found with email $email"

fun roleNotFound(id: Long): String = "role not found with id $id"

fun userAlreadyHasRole(roleId: Long, userId: Long): String = "user $userId already has role $roleId"

fun userNotInRole(roleId: Long, userId: Long): String = "user $userId doesn't have role $roleId"
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ class GenerationService(
return buildGetGenerationDto(generation)
}

fun getGenerationByIdOrInferLatest(id: Long?): Generation {
return if (id != null) {
repository.findById(id).orElseThrow {
NoSuchElementException(ErrorMessages.generationNotFound(id))
}
} else {
repository.findFirstByOrderBySchoolYearDesc()
?: throw IllegalArgumentException(ErrorMessages.noGenerations)
}
}
fun getGenerationByYear(year: String): GetGenerationDto {
val generation =
repository.findBySchoolYear(year) ?: throw NoSuchElementException(ErrorMessages.generationNotFound(year))
Expand Down Expand Up @@ -96,19 +106,15 @@ class GenerationService(
roleDto.accountIds.forEach {
val account = accountService.getAccountById(it)

// only owner side is needed after transaction, but it's useful to update the objects
account.roles.add(role)
role.accounts.add(account)
}

roleDto.associatedActivities.forEachIndexed associatedLoop@{ activityRoleIdx, activityRoleDto ->
val perActivityRole = role.associatedActivities[activityRoleIdx]
val activityId = activityRoleDto.activityId ?: return@associatedLoop
val activity = activityService.getActivityById(activityId)

// only owner side is needed after transaction, but it's useful to update the objects
perActivityRole.activity = activity
activity.associatedRoles.add(perActivityRole)
}
}
}
Expand Down
101 changes: 84 additions & 17 deletions src/main/kotlin/pt/up/fe/ni/website/backend/service/RoleService.kt
Original file line number Diff line number Diff line change
@@ -1,46 +1,113 @@
package pt.up.fe.ni.website.backend.service

import jakarta.transaction.Transactional
import jakarta.validation.Validator
import org.springframework.stereotype.Service
import pt.up.fe.ni.website.backend.model.Activity
import pt.up.fe.ni.website.backend.dto.entity.role.CreateRoleDto
import pt.up.fe.ni.website.backend.dto.entity.role.UpdateRoleDto
import pt.up.fe.ni.website.backend.model.PerActivityRole
import pt.up.fe.ni.website.backend.model.Role
import pt.up.fe.ni.website.backend.model.permissions.Permission
import pt.up.fe.ni.website.backend.model.permissions.Permissions
import pt.up.fe.ni.website.backend.repository.GenerationRepository
import pt.up.fe.ni.website.backend.repository.PerActivityRoleRepository
import pt.up.fe.ni.website.backend.repository.RoleRepository
import pt.up.fe.ni.website.backend.service.activity.ActivityService

@Service
@Transactional
class RoleService(
private val roleRepository: RoleRepository,
private val perActivityRoleRepository: PerActivityRoleRepository
private val perActivityRoleRepository: PerActivityRoleRepository,
private val generationService: GenerationService,
private val accountService: AccountService,
private val activityService: ActivityService,
private val generationRepository: GenerationRepository,
private val validator: Validator
) {

fun grantPermissionToRole(role: Role, permission: Permission) {
role.permissions.add(permission)
fun getRoleById(roleId: Long): Role {
val role = roleRepository.findById(roleId).orElseThrow {
throw NoSuchElementException(ErrorMessages.roleNotFound(roleId))
}
return role
}
fun getAllRoles(): List<Role> = roleRepository.findAll().toList()
fun grantPermissionToRole(roleId: Long, permissions: Permissions) {
val role = getRoleById(roleId)
role.permissions.addAll(permissions)
roleRepository.save(role)
}

fun revokePermissionFromRole(role: Role, permission: Permission) {
role.permissions.remove(permission)
fun revokePermissionFromRole(roleId: Long, permissions: Permissions) {
val role = getRoleById(roleId)
role.permissions.removeAll(permissions)
roleRepository.save(role)
}

fun grantPermissionToRoleOnActivity(role: Role, activity: Activity, permission: Permission) {
val foundActivity = activity.associatedRoles
fun grantPermissionToRoleOnActivity(roleId: Long, activityId: Long, permissions: Permissions) {
val activity = activityService.getActivityById(activityId)
val role = getRoleById(roleId)
val perActivityRole = activity.associatedRoles
.find { it.activity == activity } ?: PerActivityRole(Permissions())
foundActivity.role = role
foundActivity.activity = activity
perActivityRole.role = role
perActivityRole.activity = activity

foundActivity.role = role
foundActivity.permissions.add(permission)
perActivityRoleRepository.save(foundActivity)
perActivityRole.permissions.addAll(permissions)
perActivityRoleRepository.save(perActivityRole)
}

fun revokePermissionFromRoleOnActivity(role: Role, activity: Activity, permission: Permission) {
fun revokePermissionFromRoleOnActivity(roleId: Long, activityId: Long, permissions: Permissions) {
val activity = activityService.getActivityById(activityId)
val role = getRoleById(roleId)
val foundActivity = activity.associatedRoles
.find { it.role == role } ?: return

foundActivity.permissions.remove(permission)
foundActivity.permissions.removeAll(permissions)
perActivityRoleRepository.save(foundActivity)
}

fun createNewRole(dto: CreateRoleDto): Role {
val role = dto.create()
val generation = generationService.getGenerationByIdOrInferLatest(dto.generationId)

generation.roles.add(role) // just for validation and will not be persisted
if (validator.validateProperty(generation, "roles").isNotEmpty()) {
throw IllegalArgumentException(ErrorMessages.roleAlreadyExists(role.name, generation.schoolYear))
}
role.generation = generation
roleRepository.save(role)
return role
}

fun deleteRole(roleId: Long) {
val role = getRoleById(roleId)
role.generation.roles.remove(role)
generationRepository.save(role.generation)
roleRepository.delete(role)
}

fun addUserToRole(roleId: Long, userId: Long) {
val role = getRoleById(roleId)
val account = accountService.getAccountById(userId)
if (role.accounts.any { it.id == account.id }) return
account.roles.add(role)
roleRepository.save(role)
}

fun removeUserFromRole(roleId: Long, userId: Long) {
val role = getRoleById(roleId)
val account = accountService.getAccountById(userId)
role.accounts.remove(account)
roleRepository.save(role)
}

fun updateRole(roleId: Long, dto: UpdateRoleDto): Role {
val role = getRoleById(roleId)

dto.update(role)
if (validator.validateProperty(role.generation, "roles").isNotEmpty()) {
throw IllegalArgumentException(ErrorMessages.roleAlreadyExists(role.name, role.generation.schoolYear))
}

return roleRepository.save(role)
}
}
Loading

0 comments on commit 4d457a4

Please sign in to comment.