feat(fullstack): Set user roles per orga, scope notification to orga and role, add orga and role to JWT

This commit is contained in:
2025-09-15 19:23:06 +02:00
parent 83f1fa71b6
commit e3643d8318
25 changed files with 575 additions and 287 deletions

View File

@@ -1,6 +1,5 @@
package com.betriebsratkanzlei.legalconsenthub.user
import com.betriebsratkanzlei.legalconsenthub_api.model.UserRole
import com.betriebsratkanzlei.legalconsenthub_api.model.UserStatus
import jakarta.persistence.*
import org.springframework.data.annotation.CreatedDate
@@ -22,10 +21,10 @@ class User(
@Enumerated(EnumType.STRING)
@Column(nullable = false)
var status: UserStatus = UserStatus.ACTIVE,
@Enumerated(EnumType.STRING)
@Column(nullable = true)
var role: UserRole? = null,
@ElementCollection
@CollectionTable(name = "user_organization_roles", joinColumns = [JoinColumn(name = "user_id")])
var organizationRoles: MutableSet<UserOrganizationRole> = mutableSetOf(),
@CreatedDate
@Column(nullable = false)

View File

@@ -1,9 +1,11 @@
package com.betriebsratkanzlei.legalconsenthub.user
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
import com.betriebsratkanzlei.legalconsenthub_api.api.UserApi
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateUserDto
import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.RestController
@RestController
@@ -22,8 +24,24 @@ class UserController(
return ResponseEntity.ok(userMapper.toUserDto(user))
}
override fun updateUser(id: String, userDto: UserDto?): ResponseEntity<UserDto> {
val user = if (userDto != null) {
// Update with provided data
userService.updateUser(id, userDto)
} else {
// Update from JWT data
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
val userId = principal.id ?: throw IllegalArgumentException("User ID missing from JWT")
val organizationId = principal.organizationId
val roles = principal.roles
userService.updateUserFromJwt(userId, organizationId, roles)
}
return ResponseEntity.ok(userMapper.toUserDto(user))
}
override fun deleteUser(id: String): ResponseEntity<Unit> {
userService.deleteUser(id)
return ResponseEntity.noContent().build()
}
}
}

View File

@@ -4,22 +4,31 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
import org.springframework.stereotype.Component
@Component
class UserMapper() {
class UserMapper(
private val roleConverter: UserRoleConverter
) {
fun toUserDto(user: User): UserDto {
val organizationRolesDto = roleConverter.convertToMap(user.organizationRoles)
return UserDto(
id = user.id,
name = user.name,
status = user.status,
role = user.role
organizationRoles = organizationRolesDto
)
}
fun toUser(userDto: UserDto): User {
return User(
val user = User(
id = userDto.id,
name = userDto.name,
status = userDto.status,
role = userDto.role
status = userDto.status
)
userDto.organizationRoles.forEach { (orgId, roles) ->
roleConverter.setRolesForOrganization(user.organizationRoles, orgId, roles)
}
return user
}
}

View File

@@ -0,0 +1,13 @@
package com.betriebsratkanzlei.legalconsenthub.user
import jakarta.persistence.Column
import jakarta.persistence.Embeddable
@Embeddable
data class UserOrganizationRole(
@Column(name = "organization_id", nullable = false)
val organizationId: String,
@Column(name = "role", nullable = false)
val role: String
)

View File

@@ -0,0 +1,42 @@
package com.betriebsratkanzlei.legalconsenthub.user
import com.betriebsratkanzlei.legalconsenthub_api.model.UserRole
import org.springframework.stereotype.Component
@Component
object UserRoleConverter {
fun getRolesForOrganization(organizationRoles: Set<UserOrganizationRole>, organizationId: String): List<UserRole> {
return organizationRoles
.filter { it.organizationId == organizationId }
.mapNotNull { orgRole ->
try {
UserRole.valueOf(orgRole.role)
} catch (e: IllegalArgumentException) {
null
}
}
}
fun setRolesForOrganization(organizationRoles: MutableSet<UserOrganizationRole>, organizationId: String, roles: List<UserRole>) {
organizationRoles.removeIf { it.organizationId == organizationId }
roles.forEach { role ->
organizationRoles.add(UserOrganizationRole(organizationId, role.name))
}
}
fun convertToMap(organizationRoles: Set<UserOrganizationRole>): Map<String, List<UserRole>> {
return organizationRoles
.groupBy { it.organizationId }
.mapValues { (_, roles) ->
roles.mapNotNull { orgRole ->
try {
UserRole.valueOf(orgRole.role)
} catch (e: IllegalArgumentException) {
null
}
}
}
.filterValues { it.isNotEmpty() }
}
}

View File

@@ -4,13 +4,16 @@ import com.betriebsratkanzlei.legalconsenthub.error.UserAlreadyExistsException
import com.betriebsratkanzlei.legalconsenthub.error.UserNotFoundException
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateUserDto
import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
import com.betriebsratkanzlei.legalconsenthub_api.model.UserStatus
import jakarta.transaction.Transactional
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Service
@Service
class UserService(
private val userRepository: UserRepository
private val userRepository: UserRepository,
private val roleConverter: UserRoleConverter
) {
fun getCurrentUser(): User {
@@ -29,9 +32,13 @@ class UserService(
val user = User(
id = createUserDto.id,
name = createUserDto.name,
status = createUserDto.status ?: UserStatus.ACTIVE,
role = createUserDto.role
status = createUserDto.status
)
createUserDto.organizationRoles?.forEach { (orgId, roles) ->
roleConverter.setRolesForOrganization(user.organizationRoles, orgId, roles)
}
return userRepository.save(user)
}
@@ -40,6 +47,44 @@ class UserService(
.orElseThrow { UserNotFoundException(userId) }
}
@Transactional
fun updateUser(userId: String, userDto: UserDto): User {
val user = userRepository.findById(userId)
.orElseThrow { UserNotFoundException(userId) }
user.name = userDto.name
user.status = userDto.status
user.organizationRoles.clear()
userDto.organizationRoles.forEach { (orgId, roles) ->
roleConverter.setRolesForOrganization(user.organizationRoles, orgId, roles)
}
return userRepository.save(user)
}
@Transactional
fun updateUserFromJwt(userId: String, jwtOrganizationId: String?, jwtRoles: List<String>?): User {
val existingUser = userRepository.findById(userId)
.orElseThrow { UserNotFoundException(userId) }
if (jwtOrganizationId != null && !jwtRoles.isNullOrEmpty()) {
existingUser.organizationRoles.removeIf { it.organizationId == jwtOrganizationId }
jwtRoles.forEach { role ->
val normalizedRole = role.lowercase().replace("_", "_")
existingUser.organizationRoles.add(
UserOrganizationRole(
organizationId = jwtOrganizationId,
role = normalizedRole
)
)
}
}
return userRepository.save(existingUser)
}
fun deleteUser(userId: String) {
userRepository.deleteById(userId)
}