#25 Migrate users and groups to new logic

This commit is contained in:
FyloZ 2022-04-20 22:17:38 -04:00
parent 129fc4dcb9
commit d0965d75a0
Signed by: william
GPG Key ID: 835378AE9AF4AE97
51 changed files with 975 additions and 1707 deletions

View File

@ -4,11 +4,14 @@ object Constants {
object ControllerPaths {
const val COMPANY = "/api/company"
const val FILE = "/api/file"
const val GROUP = "/api/user/group"
const val INVENTORY = "/api/inventory"
const val MATERIAL = "/api/material"
const val MATERIAL_TYPE = "/api/materialtype"
const val MIX = "/api/recipe/mix"
const val RECIPE = "/api/recipe"
const val TOUCH_UP_KIT = "/api/touchupkit"
const val USER = "/api/user"
}
object FilePaths {
@ -22,6 +25,7 @@ object Constants {
object ModelNames {
const val COMPANY = "Company"
const val GROUP = "Group"
const val MATERIAL = "Material"
const val MATERIAL_TYPE = "MaterialType"
const val MIX = "Mix"
@ -30,12 +34,14 @@ object Constants {
const val RECIPE = "Recipe"
const val RECIPE_STEP = "RecipeStep"
const val TOUCH_UP_KIT = "TouchUpKit"
const val USER = "User"
}
object ValidationMessages {
const val SIZE_GREATER_OR_EQUALS_ZERO = "Must be greater or equals to 0"
const val SIZE_GREATER_OR_EQUALS_ONE = "Must be greater or equals to 1"
const val RANGE_OUTSIDE_PERCENTS = "Must be between 0 and 100"
const val PASSWORD_TOO_SMALL = "Must contains at least 8 characters"
}
object ValidationRegexes {

View File

@ -2,13 +2,12 @@ package dev.fyloz.colorrecipesexplorer.config.security
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.dtos.UserLoginRequestDto
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
import dev.fyloz.colorrecipesexplorer.model.account.UserLoginRequest
import dev.fyloz.colorrecipesexplorer.model.account.UserOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.toAuthorities
import dev.fyloz.colorrecipesexplorer.utils.addCookie
import io.jsonwebtoken.ExpiredJwtException
import org.springframework.security.authentication.AuthenticationManager
@ -40,7 +39,7 @@ class JwtAuthenticationFilter(
}
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequest::class.java)
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequestDto::class.java)
logger.debug("Login attempt for user ${loginRequest.id}...")
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
}
@ -116,8 +115,8 @@ class JwtAuthorizationFilter(
}
}
private fun getAuthenticationToken(user: UserOutputDto) =
UsernamePasswordAuthenticationToken(user.id, null, user.permissions.toAuthorities())
private fun getAuthenticationToken(user: UserDto) =
UsernamePasswordAuthenticationToken(user.id, null, user.authorities)
private fun getAuthenticationToken(userId: Long): UsernamePasswordAuthenticationToken? = try {
val userDetails = userDetailsLogic.loadUserById(userId)

View File

@ -1,12 +1,12 @@
package dev.fyloz.colorrecipesexplorer.config.security
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.emergencyMode
import dev.fyloz.colorrecipesexplorer.logic.users.JwtLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserDetailsLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.User
import mu.KotlinLogging
import org.slf4j.Logger
import org.springframework.boot.context.properties.EnableConfigurationProperties
@ -147,13 +147,14 @@ class SecurityConfig(
with(securityProperties.root!!) {
if (!userLogic.existsById(this.id)) {
userLogic.save(
User(
UserDto(
id = this.id,
firstName = rootUserFirstName,
lastName = rootUserLastName,
group = null,
password = passwordEncoder.encode(this.password),
isSystemUser = true,
permissions = mutableSetOf(Permission.ADMIN)
permissions = listOf(Permission.ADMIN),
isSystemUser = true
)
)
}

View File

@ -0,0 +1,25 @@
package dev.fyloz.colorrecipesexplorer.dtos
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
data class GroupDto(
override val id: Long = 0L,
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: List<Permission>,
val explicitPermissions: List<Permission> = listOf()
) : EntityDto {
@get:JsonIgnore
val defaultGroupUserId = getDefaultGroupUserId(id)
companion object {
fun getDefaultGroupUserId(id: Long) = 1000000 + id
}
}

View File

@ -2,7 +2,6 @@ package dev.fyloz.colorrecipesexplorer.dtos
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.model.account.Group
import java.time.LocalDate
import javax.validation.constraints.Max
import javax.validation.constraints.Min
@ -94,7 +93,7 @@ data class RecipeUpdateDto(
data class RecipeGroupInformationDto(
override val id: Long = 0L,
val group: Group,
val group: GroupDto,
val note: String? = null,

View File

@ -0,0 +1,94 @@
package dev.fyloz.colorrecipesexplorer.dtos
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.toAuthority
import java.time.LocalDateTime
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size
data class UserDto(
override val id: Long = 0L,
val firstName: String,
val lastName: String,
@field:JsonIgnore
val password: String = "",
val group: GroupDto?,
val permissions: List<Permission>,
val explicitPermissions: List<Permission> = listOf(),
val lastLoginTime: LocalDateTime? = null,
@field:JsonIgnore
val isDefaultGroupUser: Boolean = false,
@field:JsonIgnore
val isSystemUser: Boolean = false
) : EntityDto {
@get:JsonIgnore
val authorities
get() = permissions
.map { it.toAuthority() }
.toMutableSet()
}
data class UserSaveDto(
val id: Long = 0L,
@field:NotBlank
val firstName: String,
@field:NotBlank
val lastName: String,
@field:NotBlank
@field:Size(min = 8, message = Constants.ValidationMessages.PASSWORD_TOO_SMALL)
val password: String,
val groupId: Long?,
val permissions: List<Permission>,
// TODO WN: Test if working
// @JsonProperty(access = JsonProperty.Access.READ_ONLY)
@field:JsonIgnore
val isSystemUser: Boolean = false,
@field:JsonIgnore
val isDefaultGroupUser: Boolean = false
)
data class UserUpdateDto(
val id: Long = 0L,
@field:NotBlank
val firstName: String,
@field:NotBlank
val lastName: String,
val groupId: Long?,
val permissions: List<Permission>
)
data class UserLoginRequestDto(val id: Long, val password: String)
class UserDetails(val user: UserDto) : SpringUserDetails {
override fun getPassword() = user.password
override fun getUsername() = user.id.toString()
override fun getAuthorities() = user.authorities
override fun isAccountNonExpired() = true
override fun isAccountNonLocked() = true
override fun isCredentialsNonExpired() = true
override fun isEnabled() = true
}

View File

@ -0,0 +1,10 @@
package dev.fyloz.colorrecipesexplorer.exception
import org.springframework.http.HttpStatus
class NoDefaultGroupException : RestException(
"nodefaultgroup",
"No default group",
HttpStatus.NOT_FOUND,
"No default group cookie is defined in the current request"
)

View File

@ -94,17 +94,6 @@ abstract class BaseLogic<D : EntityDto, S : Service<D, *, *>>(
details
)
private fun loadRelations(dto: D, relationSelectors: Collection<(D) -> Iterable<*>>) {
relationSelectors.map { it(dto) }
.forEach {
if (it is LazyMapList<*, *>) {
it.initialize()
} else {
println("Can't load :(")
}
}
}
companion object {
const val ID_IDENTIFIER_NAME = "id"
const val NAME_IDENTIFIER_NAME = "name"

View File

@ -1,182 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity
import dev.fyloz.colorrecipesexplorer.repository.NamedJpaRepository
import io.jsonwebtoken.lang.Assert
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.findByIdOrNull
/**
* A service implementing the basics CRUD operations for the given entities.
*
* @param E The entity type
* @param R The entity repository type
*/
interface OldService<E, R : JpaRepository<E, *>> {
val repository: R
/** Gets all entities. */
fun getAll(): Collection<E>
/** Saves a given [entity]. */
fun save(entity: E): E
/** Updates a given [entity]. */
fun update(entity: E): E
/** Deletes a given [entity]. */
fun delete(entity: E)
}
/** A service for entities implementing the [ModelEntity] interface. This service add supports for numeric identifiers. */
interface ModelService<E : ModelEntity, R : JpaRepository<E, *>> : OldService<E, R> {
/** Checks if an entity with the given [id] exists. */
fun existsById(id: Long): Boolean
/** Gets the entity with the given [id]. */
fun getById(id: Long): E
/** Deletes the entity with the given [id]. */
fun deleteById(id: Long)
}
/** A service for entities implementing the [NamedModelEntity] interface. This service add supports for name identifiers. */
interface NamedModelService<E : NamedModelEntity, R : JpaRepository<E, *>> : ModelService<E, R> {
/** Checks if an entity with the given [name] exists. */
fun existsByName(name: String): Boolean
/** Gets the entity with the given [name]. */
fun getByName(name: String): E
}
abstract class AbstractService<E, R : JpaRepository<E, *>>(override val repository: R) : OldService<E, R> {
override fun getAll(): Collection<E> = repository.findAll()
override fun save(entity: E): E = repository.save(entity)
override fun update(entity: E): E = repository.save(entity)
override fun delete(entity: E) = repository.delete(entity)
}
abstract class AbstractModelService<E : ModelEntity, R : JpaRepository<E, Long>>(repository: R) :
AbstractService<E, R>(repository), ModelService<E, R> {
protected abstract fun idNotFoundException(id: Long): NotFoundException
protected abstract fun idAlreadyExistsException(id: Long): AlreadyExistsException
override fun existsById(id: Long): Boolean = repository.existsById(id)
override fun getById(id: Long): E = repository.findByIdOrNull(id) ?: throw idNotFoundException(id)
override fun save(entity: E): E {
if (entity.id != null && existsById(entity.id!!))
throw idAlreadyExistsException(entity.id!!)
return super.save(entity)
}
override fun update(entity: E): E {
assertId(entity.id)
if (!existsById(entity.id!!))
throw idNotFoundException(entity.id!!)
return super.update(entity)
}
override fun deleteById(id: Long) =
delete(getById(id)) // Use delete(entity) to prevent code duplication and to ease testing
protected fun assertId(id: Long?) {
Assert.notNull(id, "${javaClass.simpleName}.update() was called with a null identifier")
}
}
abstract class AbstractNamedModelService<E : NamedModelEntity, R : NamedJpaRepository<E>>(repository: R) :
AbstractModelService<E, R>(repository), NamedModelService<E, R> {
protected abstract fun nameNotFoundException(name: String): NotFoundException
protected abstract fun nameAlreadyExistsException(name: String): AlreadyExistsException
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getByName(name: String): E = repository.findByName(name) ?: throw nameNotFoundException(name)
override fun save(entity: E): E {
if (existsByName(entity.name))
throw nameAlreadyExistsException(entity.name)
return super.save(entity)
}
override fun update(entity: E): E {
assertId(entity.id)
assertName(entity.name)
with(repository.findByName(entity.name)) {
if (this != null && id != entity.id)
throw nameAlreadyExistsException(entity.name)
}
return super.update(entity)
}
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 the REST API, for example.
*
* @param E The entity type
* @param S The entity save DTO type
* @param U The entity update DTO type
*/
interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> : OldService<E, R> {
/** Gets all entities mapped to their output model. */
fun getAllForOutput(): Collection<O>
/** Saves a given [entity]. */
fun save(entity: S): E = save(entity.toEntity())
/** Updates a given [entity]. */
fun update(entity: U): E
/** Convert the given entity to its output model. */
fun E.toOutput(): O
}
/** An [ExternalService] for entities implementing the [ModelEntity] interface. */
interface ExternalModelService<E : ModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
ModelService<E, R>, ExternalService<E, S, U, O, R> {
/** Gets the entity with the given [id] mapped to its output model. */
fun getByIdForOutput(id: Long): O
}
/** An [ExternalService] for entities implementing the [NamedModelEntity] interface. */
interface ExternalNamedModelService<E : NamedModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
NamedModelService<E, R>, ExternalModelService<E, S, U, O, R>
/** An [AbstractService] with the functionalities of a [ExternalService]. */
@Suppress("unused")
abstract class AbstractExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>>(repository: R) :
AbstractService<E, R>(repository), ExternalService<E, S, U, O, R> {
override fun getAllForOutput() =
getAll().map { it.toOutput() }
}
/** An [AbstractModelService] with the functionalities of a [ExternalService]. */
abstract class AbstractExternalModelService<E : ModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, Long>>(
repository: R
) : AbstractModelService<E, R>(repository), ExternalModelService<E, S, U, O, R> {
override fun getAllForOutput() =
getAll().map { it.toOutput() }
override fun getByIdForOutput(id: Long) =
getById(id).toOutput()
}
/** An [AbstractNamedModelService] with the functionalities of a [ExternalService]. */
abstract class AbstractExternalNamedModelService<E : NamedModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : NamedJpaRepository<E>>(
repository: R
) : AbstractNamedModelService<E, R>(repository), ExternalNamedModelService<E, S, U, O, R> {
override fun getAllForOutput() =
getAll().map { it.toOutput() }
override fun getByIdForOutput(id: Long) =
getById(id).toOutput()
}

View File

@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
@ -30,7 +31,7 @@ class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) :
}
class InvalidGroupStepsPositionsException(
val group: Group,
val group: GroupDto,
val exception: InvalidPositionsException
) : RestException(
"invalid-groupinformation-recipestep-position",
@ -39,7 +40,7 @@ class InvalidGroupStepsPositionsException(
"The position of steps for the group ${group.name} are invalid",
mapOf(
"group" to group.name,
"groupId" to group.id!!,
"groupId" to group.id,
"invalidSteps" to exception.errors
)
) {

View File

@ -1,97 +1,80 @@
package dev.fyloz.colorrecipesexplorer.logic.users
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.logic.AbstractExternalNamedModelService
import dev.fyloz.colorrecipesexplorer.logic.ExternalNamedModelService
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.exception.NoDefaultGroupException
import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
import dev.fyloz.colorrecipesexplorer.logic.Logic
import dev.fyloz.colorrecipesexplorer.service.GroupService
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.util.WebUtils
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.transaction.Transactional
const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
interface GroupLogic :
ExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository> {
interface GroupLogic : Logic<GroupDto, GroupService> {
/** Gets all the users of the group with the given [id]. */
fun getUsersForGroup(id: Long): Collection<User>
fun getUsersForGroup(id: Long): Collection<UserDto>
/** Gets the default group from a cookie in the given HTTP [request]. */
fun getRequestDefaultGroup(request: HttpServletRequest): Group
fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto
/** Sets the default group cookie for the given HTTP [response]. */
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)
fun setResponseDefaultGroup(id: Long, response: HttpServletResponse)
}
@Service
@Profile("!emergency")
class DefaultGroupLogic(
private val userLogic: UserLogic,
groupRepository: GroupRepository
) : AbstractExternalNamedModelService<Group, GroupSaveDto, GroupUpdateDto, GroupOutputDto, GroupRepository>(
groupRepository
),
@LogicComponent
class DefaultGroupLogic(service: GroupService, private val userLogic: UserLogic) :
BaseLogic<GroupDto, GroupService>(service, Constants.ModelNames.GROUP),
GroupLogic {
override fun idNotFoundException(id: Long) = groupIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = groupIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = groupNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = groupNameAlreadyExistsException(name)
override fun getUsersForGroup(id: Long) = userLogic.getAllByGroup(getById(id))
override fun Group.toOutput() = GroupOutputDto(
this.id!!,
this.name,
this.permissions,
this.flatPermissions
)
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getUsersForGroup(id: Long): Collection<User> =
userLogic.getByGroup(getById(id))
@Transactional
override fun save(entity: Group): Group {
return super<AbstractExternalNamedModelService>.save(entity).apply {
userLogic.saveDefaultGroupUser(this)
}
}
override fun update(entity: GroupUpdateDto): Group {
val persistedGroup by lazy { getById(entity.id) }
return update(with(entity) {
Group(
entity.id,
if (name.isNotBlank()) entity.name else persistedGroup.name,
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
)
})
}
@Transactional
override fun delete(entity: Group) {
userLogic.delete(userLogic.getDefaultGroupUser(entity))
super.delete(entity)
}
override fun getRequestDefaultGroup(request: HttpServletRequest): Group {
override fun getRequestDefaultGroup(request: HttpServletRequest): GroupDto {
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
?: throw NoDefaultGroupException()
val defaultGroupUser = userLogic.getById(
defaultGroupCookie.value.toLong(),
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
isSystemUser = false,
isDefaultGroupUser = true
)
return defaultGroupUser.group!!
}
override fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse) {
val group = getById(groupId)
val defaultGroupUser = userLogic.getDefaultGroupUser(group)
override fun setResponseDefaultGroup(id: Long, response: HttpServletResponse) {
val defaultGroupUser = userLogic.getDefaultGroupUser(getById(id))
response.addHeader(
"Set-Cookie",
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=$defaultGroupCookieMaxAge; Path=/api; HttpOnly; Secure; SameSite=strict"
)
}
}
@Transactional
override fun save(dto: GroupDto): GroupDto {
throwIfNameAlreadyExists(dto.name)
return super.save(dto).also {
userLogic.saveDefaultGroupUser(it)
}
}
override fun update(dto: GroupDto): GroupDto {
throwIfNameAlreadyExists(dto.name, dto.id)
return super.update(dto)
}
override fun deleteById(id: Long) {
userLogic.deleteById(GroupDto.getDefaultGroupUserId(id))
super.deleteById(id)
}
private fun throwIfNameAlreadyExists(name: String, id: Long? = null) {
if (service.existsByName(name, id)) {
throw alreadyExistsException(value = name)
}
}
}

View File

@ -3,10 +3,8 @@ package dev.fyloz.colorrecipesexplorer.logic.users
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
import dev.fyloz.colorrecipesexplorer.model.account.UserOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.toOutputDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.utils.base64encode
import dev.fyloz.colorrecipesexplorer.utils.toDate
import io.jsonwebtoken.Jwts
@ -23,10 +21,10 @@ interface JwtLogic {
fun buildJwt(userDetails: UserDetails): String
/** Build a JWT token for the given [user]. */
fun buildJwt(user: User): String
fun buildJwt(user: UserDto): String
/** Parses a user from the given [jwt] token. */
fun parseJwt(jwt: String): UserOutputDto
fun parseJwt(jwt: String): UserDto
}
@Service
@ -54,14 +52,14 @@ class DefaultJwtLogic(
override fun buildJwt(userDetails: UserDetails) =
buildJwt(userDetails.user)
override fun buildJwt(user: User): String =
override fun buildJwt(user: UserDto): String =
jwtBuilder
.setSubject(user.id.toString())
.setExpiration(getCurrentExpirationDate())
.claim(jwtClaimUser, user.serialize())
.compact()
override fun parseJwt(jwt: String): UserOutputDto =
override fun parseJwt(jwt: String): UserDto =
with(
jwtParser.parseClaimsJws(jwt)
.body.get(jwtClaimUser, String::class.java)
@ -74,6 +72,6 @@ class DefaultJwtLogic(
.plusSeconds(securityProperties.jwtDuration)
.toDate()
private fun User.serialize(): String =
objectMapper.writeValueAsString(this.toOutputDto())
private fun UserDto.serialize(): String =
objectMapper.writeValueAsString(this)
}

View File

@ -4,18 +4,18 @@ import dev.fyloz.colorrecipesexplorer.SpringUserDetails
import dev.fyloz.colorrecipesexplorer.SpringUserDetailsService
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
import dev.fyloz.colorrecipesexplorer.model.account.user
import org.springframework.context.annotation.Profile
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
interface UserDetailsLogic : SpringUserDetailsService {
/** Loads an [User] for the given [id]. */
fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean = false): UserDetails
fun loadUserById(id: Long, isDefaultGroupUser: Boolean = true): UserDetails
}
@Service
@ -25,17 +25,17 @@ class DefaultUserDetailsLogic(
) : UserDetailsLogic {
override fun loadUserByUsername(username: String): UserDetails {
try {
return loadUserById(username.toLong(), true)
return loadUserById(username.toLong(), false)
} catch (ex: NotFoundException) {
throw UsernameNotFoundException(username)
}
}
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails {
val user = userLogic.getById(
id,
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
ignoreSystemUsers = false
isSystemUser = true,
isDefaultGroupUser = isDefaultGroupUser
)
return UserDetails(user)
}
@ -46,7 +46,7 @@ class DefaultUserDetailsLogic(
class EmergencyUserDetailsLogic(
securityProperties: CreSecurityProperties
) : UserDetailsLogic {
private val users: Set<User>
private val users: Set<UserDto>
init {
if (securityProperties.root == null) {
@ -56,20 +56,23 @@ class EmergencyUserDetailsLogic(
users = setOf(
// Add root user
with(securityProperties.root!!) {
user(
UserDto(
id = this.id,
plainPassword = this.password,
permissions = mutableSetOf(Permission.ADMIN)
firstName = "Root",
lastName = "User",
group = null,
password = this.password,
permissions = listOf(Permission.ADMIN)
)
}
)
}
override fun loadUserByUsername(username: String): SpringUserDetails {
return loadUserById(username.toLong(), true)
return loadUserById(username.toLong(), false)
}
override fun loadUserById(id: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
override fun loadUserById(id: Long, isDefaultGroupUser: Boolean): UserDetails {
val user = users.firstOrNull { it.id == id }
?: throw UsernameNotFoundException(id.toString())

View File

@ -1,189 +1,146 @@
package dev.fyloz.colorrecipesexplorer.logic.users
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.config.security.authorizationCookieName
import dev.fyloz.colorrecipesexplorer.config.security.blacklistedJwtTokens
import dev.fyloz.colorrecipesexplorer.logic.AbstractExternalModelService
import dev.fyloz.colorrecipesexplorer.logic.ExternalModelService
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.logic.BaseLogic
import dev.fyloz.colorrecipesexplorer.logic.Logic
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.service.UserService
import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.util.WebUtils
import java.time.LocalDateTime
import javax.servlet.http.HttpServletRequest
interface UserLogic :
ExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository> {
/** Check if an [User] with the given [firstName] and [lastName] exists. */
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
interface UserLogic : Logic<UserDto, UserService> {
/** Gets all users which have the given [group]. */
fun getAllByGroup(group: GroupDto): Collection<UserDto>
/** Gets the user with the given [id]. */
fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User
/** Gets all users which have the given [group]. */
fun getByGroup(group: Group): Collection<User>
fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto
/** Gets the default user of the given [group]. */
fun getDefaultGroupUser(group: Group): User
fun getDefaultGroupUser(group: GroupDto): UserDto
/** Save a default group user for the given [group]. */
fun saveDefaultGroupUser(group: Group)
fun saveDefaultGroupUser(group: GroupDto)
/** Updates de given [entity]. **/
fun update(entity: User, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User
/** Saves the given [dto]. */
fun save(dto: UserSaveDto): UserDto
/** Updates the last login time of the user with the given [userId]. */
fun updateLastLoginTime(userId: Long, time: LocalDateTime = LocalDateTime.now()): User
/** Updates the given [dto]. */
fun update(dto: UserUpdateDto): UserDto
/** Updates the last login time of the user with the given [id]. */
fun updateLastLoginTime(id: Long, time: LocalDateTime = LocalDateTime.now()): UserDto
/** Updates the password of the user with the given [id]. */
fun updatePassword(id: Long, password: String): User
fun updatePassword(id: Long, password: String): UserDto
/** Adds the given [permission] to the user with the given [userId]. */
fun addPermission(userId: Long, permission: Permission): User
/** Adds the given [permission] to the user with the given [id]. */
fun addPermission(id: Long, permission: Permission): UserDto
/** Removes the given [permission] from the user with the given [userId]. */
fun removePermission(userId: Long, permission: Permission): User
/** Removes the given [permission] from the user with the given [id]. */
fun removePermission(id: Long, permission: Permission): UserDto
/** Logout an user. Add the authorization token of the given [request] to the blacklisted tokens. */
/** Logout a user. Add the authorization token of the given [request] to the blacklisted tokens. */
fun logout(request: HttpServletRequest)
}
@Service
@Profile("!emergency")
@LogicComponent
class DefaultUserLogic(
userRepository: UserRepository,
@Lazy val groupLogic: GroupLogic,
) : AbstractExternalModelService<User, UserSaveDto, UserUpdateDto, UserOutputDto, UserRepository>(
userRepository
),
UserLogic {
override fun idNotFoundException(id: Long) = userIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = userIdAlreadyExistsException(id)
service: UserService, @Lazy private val groupLogic: GroupLogic, @Lazy private val passwordEncoder: PasswordEncoder
) : BaseLogic<UserDto, UserService>(service, Constants.ModelNames.USER), UserLogic {
override fun getAll() = service.getAll(isSystemUser = false, isDefaultGroupUser = false)
override fun User.toOutput() = this.toOutputDto()
override fun getAllByGroup(group: GroupDto) = service.getAllByGroup(group)
override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean =
repository.existsByFirstNameAndLastName(firstName, lastName)
override fun getById(id: Long) = getById(id, isSystemUser = false, isDefaultGroupUser = false)
override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean) =
service.getById(id, !isDefaultGroupUser, !isSystemUser) ?: throw notFoundException(value = id)
override fun getAll(): Collection<User> =
super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser }
override fun getDefaultGroupUser(group: GroupDto) =
service.getDefaultGroupUser(group) ?: throw notFoundException(identifierName = "groupId", value = group.id)
override fun getById(id: Long): User =
getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User =
super.getById(id).apply {
if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser)
throw idNotFoundException(id)
}
override fun getByGroup(group: Group): Collection<User> =
repository.findAllByGroup(group).filter {
!it.isSystemUser && !it.isDefaultGroupUser
}
override fun getDefaultGroupUser(group: Group): User =
repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)
override fun save(entity: UserSaveDto): User =
save(with(entity) {
user(
id = id,
firstName = firstName,
lastName = lastName,
plainPassword = password,
isDefaultGroupUser = false,
isSystemUser = false,
group = if (groupId != null) groupLogic.getById(groupId) else null,
permissions = permissions
)
})
override fun save(entity: User): User {
if (existsById(entity.id))
throw userIdAlreadyExistsException(entity.id)
if (existsByFirstNameAndLastName(entity.firstName, entity.lastName))
throw userFullNameAlreadyExistsException(entity.firstName, entity.lastName)
return super<AbstractExternalModelService>.save(entity)
}
override fun saveDefaultGroupUser(group: Group) {
override fun saveDefaultGroupUser(group: GroupDto) {
save(
user(
id = 1000000L + group.id!!,
UserSaveDto(
id = group.defaultGroupUserId,
firstName = group.name,
lastName = "User",
plainPassword = group.name,
group = group,
password = group.name,
groupId = group.id,
permissions = listOf(),
isDefaultGroupUser = true
)
)
}
override fun updateLastLoginTime(userId: Long, time: LocalDateTime): User {
val user = getById(userId, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false)
user.lastLoginTime = time
override fun save(dto: UserSaveDto) = save(
UserDto(
id = dto.id,
firstName = dto.firstName,
lastName = dto.lastName,
password = passwordEncoder.encode(dto.password),
group = if (dto.groupId != null) groupLogic.getById(dto.groupId) else null,
permissions = dto.permissions,
isSystemUser = dto.isSystemUser,
isDefaultGroupUser = dto.isDefaultGroupUser
)
)
override fun save(dto: UserDto): UserDto {
throwIfIdAlreadyExists(dto.id)
throwIfFirstNameAndLastNameAlreadyExists(dto.firstName, dto.lastName)
return super.save(dto)
}
override fun update(dto: UserUpdateDto): UserDto {
val user = getById(dto.id, isSystemUser = false, isDefaultGroupUser = false)
return update(
user,
ignoreDefaultGroupUsers = true,
ignoreSystemUsers = false
user.copy(
firstName = dto.firstName,
lastName = dto.lastName,
group = if (dto.groupId != null) groupLogic.getById(dto.groupId) else null,
permissions = dto.permissions
)
)
}
override fun update(entity: UserUpdateDto): User {
val persistedUser by lazy { getById(entity.id) }
return update(with(entity) {
User(
id = id,
firstName = firstName ?: persistedUser.firstName,
lastName = lastName ?: persistedUser.lastName,
password = persistedUser.password,
isDefaultGroupUser = false,
isSystemUser = false,
group = if (entity.groupId != null) groupLogic.getById(entity.groupId) else persistedUser.group,
permissions = permissions?.toMutableSet() ?: persistedUser.permissions,
lastLoginTime = persistedUser.lastLoginTime
)
})
override fun update(dto: UserDto): UserDto {
throwIfFirstNameAndLastNameAlreadyExists(dto.firstName, dto.lastName, dto.id)
return super.update(dto)
}
override fun update(entity: User): User =
update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
override fun update(entity: User, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): User {
with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
if (this != null && id != entity.id)
throw userFullNameAlreadyExistsException(entity.firstName, entity.lastName)
}
return super.update(entity)
override fun updateLastLoginTime(id: Long, time: LocalDateTime) = with(getById(id)) {
update(this.copy(lastLoginTime = time))
}
override fun updatePassword(id: Long, password: String): User {
val persistedUser = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
return super.update(with(persistedUser) {
user(
id,
firstName,
lastName,
plainPassword = password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
)
})
override fun updatePassword(id: Long, password: String) = with(getById(id)) {
update(this.copy(password = passwordEncoder.encode(password)))
}
override fun addPermission(userId: Long, permission: Permission): User =
super.update(getById(userId).apply { permissions += permission })
override fun addPermission(id: Long, permission: Permission) = with(getById(id)) {
update(this.copy(permissions = this.permissions + permission))
}
override fun removePermission(userId: Long, permission: Permission): User =
super.update(getById(userId).apply { permissions -= permission })
override fun removePermission(id: Long, permission: Permission) = with(getById(id)) {
update(this.copy(permissions = this.permissions - permission))
}
override fun logout(request: HttpServletRequest) {
val authorizationCookie = WebUtils.getCookie(request, "Authorization")
val authorizationCookie = WebUtils.getCookie(request, authorizationCookieName)
if (authorizationCookie != null) {
val authorizationToken = authorizationCookie.value
if (authorizationToken != null && authorizationToken.startsWith("Bearer")) {
@ -191,4 +148,22 @@ class DefaultUserLogic(
}
}
}
}
private fun throwIfIdAlreadyExists(id: Long) {
if (service.existsById(id)) {
throw alreadyExistsException(identifierName = ID_IDENTIFIER_NAME, value = id)
}
}
private fun throwIfFirstNameAndLastNameAlreadyExists(firstName: String, lastName: String, id: Long? = null) {
if (service.existsByFirstNameAndLastName(firstName, lastName, id)) {
throw AlreadyExistsException(
typeNameLowerCase,
"$typeName already exists",
"A $typeNameLowerCase with the name '$firstName $lastName' already exists",
"$firstName $lastName",
"fullName"
)
}
}
}

View File

@ -7,7 +7,7 @@ import javax.persistence.*
data class Company(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
@Column(unique = true)
val name: String

View File

@ -8,7 +8,7 @@ import javax.persistence.*
data class Material(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
@Column(unique = true)
val name: String,

View File

@ -8,7 +8,7 @@ import javax.persistence.*
data class MaterialType(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long? = null,
override val id: Long,
@Column(unique = true)
val name: String = "",

View File

@ -7,9 +7,9 @@ import javax.persistence.*
data class Mix(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
var location: String?,
val location: String?,
@Column(name = "recipe_id")
val recipeId: Long,

View File

@ -7,13 +7,13 @@ import javax.persistence.*
data class MixMaterial(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
@ManyToOne
@JoinColumn(name = "material_id")
val material: Material,
var quantity: Float,
val quantity: Float,
var position: Int
val position: Int
) : ModelEntity

View File

@ -7,7 +7,7 @@ import javax.persistence.*
data class MixType(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
val name: String,

View File

@ -1,17 +1,6 @@
package dev.fyloz.colorrecipesexplorer.model
/** Represents an entity, named differently to prevent conflicts with the JPA annotation. */
/** Represents an entity with an id, named differently to prevent conflicts with the JPA annotation. */
interface ModelEntity {
val id: Long?
}
interface NamedModelEntity : ModelEntity {
val name: String
}
interface EntityDto<out E> {
/** Converts the dto to an actual entity. */
fun toEntity(): E {
throw UnsupportedOperationException()
}
val id: Long
}

View File

@ -9,7 +9,7 @@ import javax.persistence.*
data class Recipe(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
/** The name of the recipe. It is not unique in the entire system, but is unique in the scope of a [Company]. */
val name: String,
@ -47,15 +47,15 @@ data class Recipe(
data class RecipeGroupInformation(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
@ManyToOne
@JoinColumn(name = "group_id")
val group: Group,
var note: String?,
val note: String?,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "recipe_group_information_id")
var steps: List<RecipeStep>?
val steps: List<RecipeStep>?
) : ModelEntity

View File

@ -7,7 +7,7 @@ import javax.persistence.*
data class RecipeStep(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
val position: Int,

View File

@ -1,134 +1,24 @@
package dev.fyloz.colorrecipesexplorer.model.account
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.http.HttpStatus
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
@Entity
@Table(name = "user_group")
data class Group(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override var id: Long? = null,
override val id: Long,
@Column(unique = true)
override val name: String = "",
val name: String,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<Permission> = mutableSetOf(),
) : NamedModelEntity {
val flatPermissions: Set<Permission>
get() = this.permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toSet()
}
open class GroupSaveDto(
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =
Group(null, name, permissions)
}
open class GroupUpdateDto(
val id: Long,
@field:NotBlank
val name: String,
@field:NotEmpty
val permissions: MutableSet<Permission>
) : EntityDto<Group> {
override fun toEntity(): Group =
Group(id, name, permissions)
}
data class GroupOutputDto(
override val id: Long,
val name: String,
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>
): ModelEntity
fun group(
id: Long? = null,
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: Group.() -> Unit = {}
) = Group(id, name, permissions).apply(op)
fun groupSaveDto(
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: GroupSaveDto.() -> Unit = {}
) = GroupSaveDto(name, permissions).apply(op)
fun groupUpdateDto(
id: Long = 0L,
name: String = "name",
permissions: MutableSet<Permission> = mutableSetOf(),
op: GroupUpdateDto.() -> Unit = {}
) = GroupUpdateDto(id, name, permissions).apply(op)
// ==== Exceptions ====
private const val GROUP_NOT_FOUND_EXCEPTION_TITLE = "Group not found"
private const val GROUP_ALREADY_EXISTS_EXCEPTION_TITLE = "Group already exists"
private const val GROUP_EXCEPTION_ERROR_CODE = "group"
class NoDefaultGroupException : RestException(
"nodefaultgroup",
"No default group",
HttpStatus.NOT_FOUND,
"No default group cookie is defined in the current request"
)
fun groupIdNotFoundException(id: Long) =
NotFoundException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_NOT_FOUND_EXCEPTION_TITLE,
"A group with the id $id could not be found",
id
)
fun groupNameNotFoundException(name: String) =
NotFoundException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_NOT_FOUND_EXCEPTION_TITLE,
"A group with the name $name could not be found",
name,
"name"
)
fun groupIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
"A group with the id $id already exists",
id,
)
fun groupNameAlreadyExistsException(name: String) =
AlreadyExistsException(
GROUP_EXCEPTION_ERROR_CODE,
GROUP_ALREADY_EXISTS_EXCEPTION_TITLE,
"A group with the name $name already exists",
name,
"name"
)
val permissions: List<Permission>,
) : ModelEntity

View File

@ -1,20 +1,10 @@
package dev.fyloz.colorrecipesexplorer.model.account
import dev.fyloz.colorrecipesexplorer.SpringUserDetails
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size
private const val VALIDATION_PASSWORD_LENGTH = "Must contains at least 8 characters"
@Entity
@Table(name = "user")
@ -23,210 +13,31 @@ data class User(
override val id: Long,
@Column(name = "first_name")
val firstName: String = "",
val firstName: String,
@Column(name = "last_name")
val lastName: String = "",
val lastName: String,
val password: String = "",
val password: String,
@Column(name = "default_group_user")
val isDefaultGroupUser: Boolean = false,
val isDefaultGroupUser: Boolean,
@Column(name = "system_user")
val isSystemUser: Boolean = false,
val isSystemUser: Boolean,
@ManyToOne
@JoinColumn(name = "group_id")
@Fetch(FetchMode.SELECT)
var group: Group? = null,
val group: Group?,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_permission", joinColumns = [JoinColumn(name = "user_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<Permission> = mutableSetOf(),
val permissions: List<Permission>,
@Column(name = "last_login_time")
var lastLoginTime: LocalDateTime? = null
) : ModelEntity {
val flatPermissions: Set<Permission>
get() = permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toMutableSet()
.apply {
if (group != null) this.addAll(group!!.flatPermissions)
}
}
open class UserSaveDto(
val id: Long,
@field:NotBlank
val firstName: String,
@field:NotBlank
val lastName: String,
@field:NotBlank
@field:Size(min = 8, message = VALIDATION_PASSWORD_LENGTH)
val password: String,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: MutableSet<Permission> = mutableSetOf()
) : EntityDto<User>
open class UserUpdateDto(
val id: Long,
@field:NotBlank
val firstName: String?,
@field:NotBlank
val lastName: String?,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: Set<Permission>?
) : EntityDto<User>
data class UserOutputDto(
override val id: Long,
val firstName: String,
val lastName: String,
val group: Group?,
val permissions: Set<Permission>,
val explicitPermissions: Set<Permission>,
val lastLoginTime: LocalDateTime?
) : ModelEntity
data class UserLoginRequest(val id: Long, val password: String)
data class UserDetails(val user: User) : SpringUserDetails {
override fun getPassword() = user.password
override fun getUsername() = user.id.toString()
override fun getAuthorities() = user.flatPermissions.toAuthorities()
override fun isAccountNonExpired() = true
override fun isAccountNonLocked() = true
override fun isCredentialsNonExpired() = true
override fun isEnabled() = true
}
// ==== DSL ====
fun user(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = "password",
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: Group? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: User.() -> Unit = {}
) = User(
id,
firstName,
lastName,
password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun user(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
plainPassword: String = "password",
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: Group? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
op: User.() -> Unit = {}
) = User(
id,
firstName,
lastName,
passwordEncoder.encode(plainPassword),
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun userSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
op: UserSaveDto.() -> Unit = {}
) = UserSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
fun userUpdateDto(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
groupId: Long? = null,
permissions: MutableSet<Permission> = mutableSetOf(),
op: UserUpdateDto.() -> Unit = {}
) = UserUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
// ==== Extensions ====
fun Set<Permission>.toAuthorities() =
this.map { it.toAuthority() }.toMutableSet()
fun User.toOutputDto() =
UserOutputDto(
this.id,
this.firstName,
this.lastName,
this.group,
this.flatPermissions,
this.permissions,
this.lastLoginTime
)
// ==== Exceptions ====
private const val USER_NOT_FOUND_EXCEPTION_TITLE = "User not found"
private const val USER_ALREADY_EXISTS_EXCEPTION_TITLE = "User already exists"
private const val USER_EXCEPTION_ERROR_CODE = "user"
fun userIdNotFoundException(id: Long) =
NotFoundException(
USER_EXCEPTION_ERROR_CODE,
USER_NOT_FOUND_EXCEPTION_TITLE,
"An user with the id $id could not be found",
id
)
fun userIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
USER_EXCEPTION_ERROR_CODE,
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
"An user with the id $id already exists",
id
)
fun userFullNameAlreadyExistsException(firstName: String, lastName: String) =
AlreadyExistsException(
USER_EXCEPTION_ERROR_CODE,
USER_ALREADY_EXISTS_EXCEPTION_TITLE,
"An user with the name '$firstName $lastName' already exists",
"$firstName $lastName",
"fullName"
)
) : ModelEntity

View File

@ -9,7 +9,7 @@ import javax.persistence.*
data class TouchUpKit(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
val project: String,
@ -41,7 +41,7 @@ data class TouchUpKit(
data class TouchUpKitProduct(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
override val id: Long,
val name: String,

View File

@ -3,18 +3,28 @@ package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.account.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
@Repository
interface UserRepository : JpaRepository<User, Long> {
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
fun findByFirstNameAndLastName(firstName: String, lastName: String): User?
/** Checks if a user with the given [firstName], [lastName] and a different [id] exists. */
fun existsByFirstNameAndLastNameAndIdNot(firstName: String, lastName: String, id: Long): Boolean
/** Finds all users for the given [group]. */
@Query("SELECT u FROM User u WHERE u.group = :group AND u.isSystemUser IS FALSE AND u.isDefaultGroupUser IS FALSE")
fun findAllByGroup(group: Group): Collection<User>
fun findByIsDefaultGroupUserIsTrueAndGroupIs(group: Group): User
/** Finds the user with the given [firstName] and [lastName]. */
fun findByFirstNameAndLastName(firstName: String, lastName: String): User?
/** Finds the default user for the given [group]. */
@Query("SELECT u FROM User u WHERE u.group = :group AND u.isDefaultGroupUser IS TRUE")
fun findDefaultGroupUser(group: Group): User?
}
@Repository
interface GroupRepository : NamedJpaRepository<Group>
interface GroupRepository : JpaRepository<Group, Long> {
/** Checks if a group with the given [name] and a different [id] exists. */
fun existsByNameAndIdNot(name: String, id: Long): Boolean
}

View File

@ -1,18 +0,0 @@
package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.NoRepositoryBean
/** Adds support for entities using a name identifier. */
@NoRepositoryBean
interface NamedJpaRepository<E : NamedModelEntity> : JpaRepository<E, Long> {
/** Checks if an entity with the given [name]. */
fun existsByName(name: String): Boolean
/** Gets the entity with the given [name]. */
fun findByName(name: String): E?
/** Removes the entity with the given [name]. */
fun deleteByName(name: String)
}

View File

@ -1,10 +1,15 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import org.springframework.context.annotation.Profile
import org.springframework.http.MediaType
import org.springframework.security.access.prepost.PreAuthorize
@ -13,30 +18,25 @@ import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.validation.Valid
private const val USER_CONTROLLER_PATH = "api/user"
private const val GROUP_CONTROLLER_PATH = "api/user/group"
@RestController
@RequestMapping(USER_CONTROLLER_PATH)
@RequestMapping(Constants.ControllerPaths.USER)
@Profile("!emergency")
class UserController(private val userLogic: UserLogic) {
@GetMapping
@PreAuthorizeViewUsers
fun getAll() =
ok(userLogic.getAllForOutput())
ok(userLogic.getAll())
@GetMapping("{id}")
@PreAuthorizeViewUsers
fun getById(@PathVariable id: Long) =
ok(userLogic.getByIdForOutput(id))
ok(userLogic.getById(id))
@PostMapping
@PreAuthorizeEditUsers
fun save(@Valid @RequestBody user: UserSaveDto) =
created<UserOutputDto>(USER_CONTROLLER_PATH) {
with(userLogic) {
save(user).toOutput()
}
created<UserDto>(Constants.ControllerPaths.USER) {
userLogic.save(user)
}
@PutMapping
@ -78,7 +78,7 @@ class UserController(private val userLogic: UserLogic) {
}
@RestController
@RequestMapping(GROUP_CONTROLLER_PATH)
@RequestMapping(Constants.ControllerPaths.GROUP)
@Profile("!emergency")
class GroupsController(
private val groupLogic: GroupLogic,
@ -87,20 +87,17 @@ class GroupsController(
@GetMapping
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
fun getAll() =
ok(groupLogic.getAllForOutput())
ok(groupLogic.getAll())
@GetMapping("{id}")
@PreAuthorizeViewUsers
fun getById(@PathVariable id: Long) =
ok(groupLogic.getByIdForOutput(id))
ok(groupLogic.getById(id))
@GetMapping("{id}/users")
@PreAuthorizeViewUsers
fun getUsersForGroup(@PathVariable id: Long) =
ok(with(userLogic) {
groupLogic.getUsersForGroup(id)
.map { it.toOutput() }
})
ok(groupLogic.getUsersForGroup(id))
@PostMapping("default/{groupId}")
@PreAuthorizeViewUsers
@ -113,27 +110,25 @@ class GroupsController(
@PreAuthorizeViewUsers
fun getRequestDefaultGroup(request: HttpServletRequest) =
ok(with(groupLogic) {
getRequestDefaultGroup(request).toOutput()
getRequestDefaultGroup(request)
})
@GetMapping("currentuser")
fun getCurrentGroupUser(request: HttpServletRequest) =
ok(with(groupLogic.getRequestDefaultGroup(request)) {
userLogic.getDefaultGroupUser(this).toOutputDto()
userLogic.getDefaultGroupUser(this)
})
@PostMapping
@PreAuthorizeEditUsers
fun save(@Valid @RequestBody group: GroupSaveDto) =
created<GroupOutputDto>(GROUP_CONTROLLER_PATH) {
with(groupLogic) {
save(group).toOutput()
}
fun save(@Valid @RequestBody group: GroupDto) =
created<GroupDto>(Constants.ControllerPaths.GROUP) {
groupLogic.save(group)
}
@PutMapping
@PreAuthorizeEditUsers
fun update(@Valid @RequestBody group: GroupUpdateDto) =
fun update(@Valid @RequestBody group: GroupDto) =
noContent {
groupLogic.update(group)
}

View File

@ -1,5 +1,6 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto
import dev.fyloz.colorrecipesexplorer.logic.InventoryLogic
@ -10,10 +11,8 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
private const val INVENTORY_CONTROLLER_PATH = "api/inventory"
@RestController
@RequestMapping(INVENTORY_CONTROLLER_PATH)
@RequestMapping(Constants.ControllerPaths.INVENTORY)
@Profile("!emergency")
class InventoryController(
private val inventoryLogic: InventoryLogic

View File

@ -44,19 +44,11 @@ fun fileCreated(basePath: String, producer: () -> String): ResponseEntity<String
return ResponseEntity.created(URI.create(path)).body(fileName)
}
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
fun <T : ModelEntity> created(controllerPath: String, body: T): ResponseEntity<T> =
created(controllerPath, body, body.id!!)
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
@JvmName("createdDto")
fun <T : EntityDto> created(controllerPath: String, body: T): ResponseEntity<T> =
created(controllerPath, body, body.id)
/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
fun <T : ModelEntity> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
created(controllerPath, producer())
/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
@JvmName("createdDto")
fun <T : EntityDto> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =

View File

@ -20,7 +20,7 @@ class DefaultCompanyService(repository: CompanyRepository) :
override fun isUsedByRecipe(id: Long) = repository.isUsedByRecipe(id)
override fun toDto(entity: Company) =
CompanyDto(entity.id!!, entity.name)
CompanyDto(entity.id, entity.name)
override fun toEntity(dto: CompanyDto) =
Company(dto.id, dto.name)

View File

@ -0,0 +1,31 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.flat
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
interface GroupService : Service<GroupDto, Group, GroupRepository> {
/** Checks if a group with the given [name] and a different [id] exists. */
fun existsByName(name: String, id: Long? = null): Boolean
/** Flatten the given the permissions of the given [group]. */
fun flattenPermissions(group: Group): List<Permission>
}
@ServiceComponent
class DefaultGroupService(repository: GroupRepository) : BaseService<GroupDto, Group, GroupRepository>(repository),
GroupService {
override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0L)
override fun toDto(entity: Group) =
GroupDto(entity.id, entity.name, flattenPermissions(entity), entity.permissions)
override fun toEntity(dto: GroupDto) =
Group(dto.id, dto.name, dto.permissions)
override fun flattenPermissions(group: Group) =
group.permissions.flatMap { it.flat() }.filter { !it.deprecated }
}

View File

@ -35,7 +35,7 @@ class DefaultMaterialService(
override fun toDto(entity: Material) =
MaterialDto(
entity.id!!,
entity.id,
entity.name,
entity.inventoryQuantity,
entity.isMixType,

View File

@ -37,7 +37,7 @@ class DefaultMaterialTypeService(repository: MaterialTypeRepository) :
override fun isUsedByMaterial(id: Long) = repository.isUsedByMaterial(id)
override fun toDto(entity: MaterialType) =
MaterialTypeDto(entity.id!!, entity.name, entity.prefix, entity.usePercentages, entity.systemType)
MaterialTypeDto(entity.id, entity.name, entity.prefix, entity.usePercentages, entity.systemType)
override fun toEntity(dto: MaterialTypeDto) =
MaterialType(dto.id, dto.name, dto.prefix, dto.usePercentages, dto.systemType)

View File

@ -16,7 +16,7 @@ class DefaultMixMaterialService(repository: MixMaterialRepository, private val m
override fun existsByMaterialId(materialId: Long) = repository.existsByMaterialId(materialId)
override fun toDto(entity: MixMaterial) =
MixMaterialDto(entity.id!!, materialService.toDto(entity.material), entity.quantity, entity.position)
MixMaterialDto(entity.id, materialService.toDto(entity.material), entity.quantity, entity.position)
override fun toEntity(dto: MixMaterialDto) =
MixMaterial(dto.id, materialService.toEntity(dto.material), dto.quantity, dto.position)

View File

@ -33,7 +33,7 @@ class DefaultMixService(
override fun toDto(entity: Mix) =
MixDto(
entity.id!!,
entity.id,
entity.location,
entity.recipeId,
mixTypeService.toDto(entity.mixType),

View File

@ -37,7 +37,7 @@ class DefaultMixTypeService(
override fun toDto(entity: MixType) =
MixTypeDto(
entity.id!!,
entity.id,
entity.name,
materialTypeService.toDto(entity.materialType),
if (entity.material != null) materialService.toDto(entity.material) else null

View File

@ -27,6 +27,7 @@ class DefaultRecipeService(
private val companyService: CompanyService,
private val mixService: MixService,
private val recipeStepService: RecipeStepService,
private val groupService: GroupService,
private val configLogic: ConfigurationLogic
) :
BaseService<RecipeDto, Recipe, RecipeRepository>(repository), RecipeService {
@ -39,7 +40,7 @@ class DefaultRecipeService(
@Transactional
override fun toDto(entity: Recipe) =
RecipeDto(
entity.id!!,
entity.id,
entity.name,
entity.description,
entity.color,
@ -55,8 +56,8 @@ class DefaultRecipeService(
private fun groupInformationToDto(entity: RecipeGroupInformation) =
RecipeGroupInformationDto(
entity.id!!,
entity.group,
entity.id,
groupService.toDto(entity.group),
entity.note,
entity.steps?.lazyMap(recipeStepService::toDto) ?: listOf()
)
@ -77,7 +78,12 @@ class DefaultRecipeService(
)
private fun groupInformationToEntity(dto: RecipeGroupInformationDto) =
RecipeGroupInformation(dto.id, dto.group, dto.note, dto.steps.map(recipeStepService::toEntity))
RecipeGroupInformation(
dto.id,
groupService.toEntity(dto.group),
dto.note,
dto.steps.map(recipeStepService::toEntity)
)
private fun isApprobationExpired(recipe: Recipe): Boolean? =
with(Period.parse(configLogic.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) {

View File

@ -11,7 +11,7 @@ interface RecipeStepService : Service<RecipeStepDto, RecipeStep, RecipeStepRepos
class DefaultRecipeStepService(repository: RecipeStepRepository) :
BaseService<RecipeStepDto, RecipeStep, RecipeStepRepository>(repository), RecipeStepService {
override fun toDto(entity: RecipeStep) =
RecipeStepDto(entity.id!!, entity.position, entity.message)
RecipeStepDto(entity.id, entity.position, entity.message)
override fun toEntity(dto: RecipeStepDto) =
RecipeStep(dto.id, dto.position, dto.message)

View File

@ -24,7 +24,7 @@ class DefaultTouchUpKitService(repository: TouchUpKitRepository, private val con
override fun toDto(entity: TouchUpKit) =
TouchUpKitDto(
entity.id!!,
entity.id,
entity.project,
entity.buggy,
entity.company,
@ -39,7 +39,7 @@ class DefaultTouchUpKitService(repository: TouchUpKitRepository, private val con
)
private fun touchUpKitProductToDto(entity: TouchUpKitProduct) =
TouchUpKitProductDto(entity.id!!, entity.name, entity.description, entity.quantity, entity.ready)
TouchUpKitProductDto(entity.id, entity.name, entity.description, entity.quantity, entity.ready)
override fun toEntity(dto: TouchUpKitDto) =
TouchUpKit(

View File

@ -0,0 +1,103 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.model.account.User
import dev.fyloz.colorrecipesexplorer.model.account.flat
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import org.springframework.data.repository.findByIdOrNull
interface UserService : Service<UserDto, User, UserRepository> {
/** Checks if a user with the given [firstName] and [lastName] exists. */
fun existsByFirstNameAndLastName(firstName: String, lastName: String, id: Long? = null): Boolean
/** Gets all users, depending on [isSystemUser] and [isDefaultGroupUser]. */
fun getAll(isSystemUser: Boolean, isDefaultGroupUser: Boolean): Collection<UserDto>
/** Gets all users for the given [group]. */
fun getAllByGroup(group: GroupDto): Collection<UserDto>
/** Finds the user with the given [id], depending on [isSystemUser] and [isDefaultGroupUser]. */
fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto?
/** Finds the user with the given [firstName] and [lastName]. */
fun getByFirstNameAndLastName(firstName: String, lastName: String): UserDto?
/** Find the default user for the given [group]. */
fun getDefaultGroupUser(group: GroupDto): UserDto?
}
@ServiceComponent
class DefaultUserService(repository: UserRepository, private val groupService: GroupService) :
BaseService<UserDto, User, UserRepository>(repository), UserService {
override fun existsByFirstNameAndLastName(firstName: String, lastName: String, id: Long?) =
repository.existsByFirstNameAndLastNameAndIdNot(firstName, lastName, id ?: 0L)
override fun getAll(isSystemUser: Boolean, isDefaultGroupUser: Boolean) =
repository.findAll()
.filter { isSystemUser || !it.isSystemUser }
.filter { isDefaultGroupUser || !it.isDefaultGroupUser }
.map(::toDto)
override fun getAllByGroup(group: GroupDto) =
repository.findAllByGroup(groupService.toEntity(group))
.map(::toDto)
override fun getById(id: Long, isSystemUser: Boolean, isDefaultGroupUser: Boolean): UserDto? {
val user = repository.findByIdOrNull(id) ?: return null
if ((!isSystemUser && user.isSystemUser) ||
!isDefaultGroupUser && user.isDefaultGroupUser
) {
return null
}
return toDto(user)
}
override fun getByFirstNameAndLastName(firstName: String, lastName: String): UserDto? {
val user = repository.findByFirstNameAndLastName(firstName, lastName)
return if (user != null) toDto(user) else null
}
override fun getDefaultGroupUser(group: GroupDto): UserDto? {
val user = repository.findDefaultGroupUser(groupService.toEntity(group))
return if (user != null) toDto(user) else null
}
override fun toDto(entity: User) = UserDto(
entity.id,
entity.firstName,
entity.lastName,
entity.password,
if (entity.group != null) groupService.toDto(entity.group) else null,
getFlattenPermissions(entity),
entity.permissions,
entity.lastLoginTime,
entity.isDefaultGroupUser,
entity.isSystemUser
)
override fun toEntity(dto: UserDto) = User(
dto.id,
dto.firstName,
dto.lastName,
dto.password,
dto.isDefaultGroupUser,
dto.isSystemUser,
if (dto.group != null) groupService.toEntity(dto.group) else null,
dto.explicitPermissions,
dto.lastLoginTime
)
private fun getFlattenPermissions(user: User): List<Permission> {
val perms = user.permissions.flatMap { it.flat() }.filter { !it.deprecated }
if (user.group != null) {
return perms + groupService.flattenPermissions(user.group)
}
return perms
}
}

View File

@ -1,349 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity
import dev.fyloz.colorrecipesexplorer.repository.NamedJpaRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.data.jpa.repository.JpaRepository
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import dev.fyloz.colorrecipesexplorer.logic.AbstractServiceTest as AbstractServiceTest1
abstract class AbstractServiceTest<E, S : OldService<E, *>, R : JpaRepository<E, *>> {
protected abstract val repository: R
protected abstract val logic: S
protected abstract val entity: E
protected abstract val anotherEntity: E
protected val entityList: List<E>
get() = listOf(
entity,
anotherEntity
)
@AfterEach
open fun afterEach() {
reset(repository, logic)
}
// getAll()
@Test
open fun `getAll() returns all available entities`() {
whenever(repository.findAll()).doReturn(entityList)
val found = logic.getAll()
assertEquals(entityList, found)
}
@Test
open fun `getAll() returns empty list when there is no entities`() {
whenever(repository.findAll()).doReturn(listOf())
val found = logic.getAll()
assertTrue { found.isEmpty() }
}
// save()
@Test
open fun `save() saves in the repository and returns the saved value`() {
whenever(repository.save(entity)).doReturn(entity)
val found = logic.save(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
// update()
@Test
open fun `update() saves in the repository and returns the updated value`() {
whenever(repository.save(entity)).doReturn(entity)
val found = logic.update(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
// delete()
@Test
open fun `delete() deletes in the repository`() {
logic.delete(entity)
verify(repository).delete(entity)
}
}
abstract class AbstractModelServiceTest<E : ModelEntity, S : ModelService<E, *>, R : JpaRepository<E, Long>> :
AbstractServiceTest1<E, S, R>() {
// existsById()
@Test
open fun `existsById() returns true when an entity with the given id exists in the repository`() {
whenever(repository.existsById(entity.id!!)).doReturn(true)
val found = logic.existsById(entity.id!!)
assertTrue(found)
}
@Test
open fun `existsById() returns false when no entity with the given id exists in the repository`() {
whenever(repository.existsById(entity.id!!)).doReturn(false)
val found = logic.existsById(entity.id!!)
assertFalse(found)
}
// getById()
@Test
open fun `getById() returns the entity with the given id from the repository`() {
whenever(repository.findById(entity.id!!)).doReturn(Optional.of(entity))
val found = logic.getById(entity.id!!)
assertEquals(entity, found)
}
@Test
open fun `getById() throws NotFoundException when no entity with the given id exists in the repository`() {
whenever(repository.findById(entity.id!!)).doReturn(Optional.empty())
assertThrows<NotFoundException> { logic.getById(entity.id!!) }
.assertErrorCode()
}
// save()
@Test
open fun `save() throws AlreadyExistsException when an entity with the given id exists in the repository`() {
doReturn(true).whenever(logic).existsById(entity.id!!)
assertThrows<AlreadyExistsException> { logic.save(entity) }
.assertErrorCode()
}
// update()
@Test
override fun `update() saves in the repository and returns the updated value`() {
whenever(repository.save(entity)).doReturn(entity)
doReturn(true).whenever(logic).existsById(entity.id!!)
doReturn(entity).whenever(logic).getById(entity.id!!)
val found = logic.update(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
@Test
open fun `update() throws NotFoundException when no entity with the given id exists in the repository`() {
doReturn(false).whenever(logic).existsById(entity.id!!)
assertThrows<NotFoundException> { logic.update(entity) }
.assertErrorCode()
}
// deleteById()
@Test
open fun `deleteById() deletes the entity with the given id in the repository`() {
doReturn(entity).whenever(logic).getById(entity.id!!)
logic.deleteById(entity.id!!)
verify(repository).delete(entity)
}
}
abstract class AbstractNamedModelServiceTest<E : NamedModelEntity, S : NamedModelService<E, *>, R : NamedJpaRepository<E>> :
AbstractModelServiceTest<E, S, R>() {
protected abstract val entityWithEntityName: E
// existsByName()
@Test
open fun `existsByName() returns true when an entity with the given name exists`() {
whenever(repository.existsByName(entity.name)).doReturn(true)
val found = logic.existsByName(entity.name)
assertTrue(found)
}
@Test
open fun `existsByName() returns false when no entity with the given name exists`() {
whenever(repository.existsByName(entity.name)).doReturn(false)
val found = logic.existsByName(entity.name)
assertFalse(found)
}
// getByName()
@Test
open fun `getByName() returns the entity with the given name`() {
whenever(repository.findByName(entity.name)).doReturn(entity)
val found = logic.getByName(entity.name)
assertEquals(entity, found)
}
@Test
open fun `getByName() throws NotFoundException when no entity with the given name exists`() {
whenever(repository.findByName(entity.name)).doReturn(null)
assertThrows<NotFoundException> { logic.getByName(entity.name) }
.assertErrorCode("name")
}
// save()
@Test
open fun `save() throws AlreadyExistsException when an entity with the given name exists`() {
doReturn(true).whenever(logic).existsByName(entity.name)
assertThrows<AlreadyExistsException> { logic.save(entity) }
.assertErrorCode("name")
}
// update()
@Test
override fun `update() saves in the repository and returns the updated value`() {
whenever(repository.save(entity)).doReturn(entity)
whenever(repository.findByName(entity.name)).doReturn(null)
doReturn(true).whenever(logic).existsById(entity.id!!)
doReturn(entity).whenever(logic).getById(entity.id!!)
val found = logic.update(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
@Test
override fun `update() throws NotFoundException when no entity with the given id exists in the repository`() {
whenever(repository.findByName(entity.name)).doReturn(null)
doReturn(false).whenever(logic).existsById(entity.id!!)
assertThrows<NotFoundException> { logic.update(entity) }
}
@Test
open fun `update() throws AlreadyExistsException when an entity with the updated name exists`() {
whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName)
doReturn(entity).whenever(logic).getById(entity.id!!)
assertThrows<AlreadyExistsException> { logic.update(entity) }
.assertErrorCode("name")
}
}
interface ExternalModelServiceTest {
fun `save(dto) calls and returns save() with the created entity`()
fun `update(dto) calls and returns update() with the created entity`()
}
// ==== IMPLEMENTATIONS FOR EXTERNAL SERVICES ====
// Lots of code duplication but I don't have a better solution for now
abstract class AbstractExternalModelServiceTest<E : ModelEntity, N : EntityDto<E>, U : EntityDto<E>, S : ExternalModelService<E, N, U, *, *>, R : JpaRepository<E, Long>> :
AbstractModelServiceTest<E, S, R>(), ExternalModelServiceTest {
protected abstract val entitySaveDto: N
protected abstract val entityUpdateDto: U
@AfterEach
override fun afterEach() {
reset(entitySaveDto, entityUpdateDto)
super.afterEach()
}
}
abstract class AbstractExternalNamedModelServiceTest<E : NamedModelEntity, N : EntityDto<E>, U : EntityDto<E>, S : ExternalNamedModelService<E, N, U, *, *>, R : NamedJpaRepository<E>> :
AbstractNamedModelServiceTest<E, S, R>(), ExternalModelServiceTest {
protected abstract val entitySaveDto: N
protected abstract val entityUpdateDto: U
@AfterEach
override fun afterEach() {
reset(entitySaveDto, entityUpdateDto)
super.afterEach()
}
}
fun NotFoundException.assertErrorCode(identifierName: String = "id") =
this.assertErrorCode("notfound", identifierName)
fun AlreadyExistsException.assertErrorCode(identifierName: String = "id") =
this.assertErrorCode("exists", identifierName)
fun RestException.assertErrorCode(type: String, identifierName: String) {
assertTrue {
this.errorCode.startsWith(type) &&
this.errorCode.endsWith(identifierName)
}
}
fun RestException.assertErrorCode(errorCode: String) {
assertEquals(errorCode, this.errorCode)
}
fun <E : ModelEntity, N : EntityDto<E>> withBaseSaveDtoTest(
entity: E,
entitySaveDto: N,
service: ExternalService<E, N, *, *, *>,
saveMockMatcher: () -> E = { entity },
op: () -> Unit = {}
) {
doReturn(entity).whenever(service).save(saveMockMatcher())
doReturn(entity).whenever(entitySaveDto).toEntity()
val found = service.save(entitySaveDto)
verify(service).save(saveMockMatcher())
assertEquals(entity, found)
op()
}
fun <E : ModelEntity, U : EntityDto<E>> withBaseUpdateDtoTest(
entity: E,
entityUpdateDto: U,
service: ExternalModelService<E, *, U, *, *>,
updateMockMatcher: () -> E,
op: E.() -> Unit = {}
) {
doAnswer { it.arguments[0] }.whenever(service).update(updateMockMatcher())
doReturn(entity).whenever(entityUpdateDto).toEntity()
doReturn(entity).whenever(service).getById(entity.id!!)
doReturn(true).whenever(service).existsById(entity.id!!)
val found = service.update(entityUpdateDto)
verify(service).update(updateMockMatcher())
assertEquals(entity, found)
found.op()
}

View File

@ -1,348 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.config.security.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.repository.GroupRepository
import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import dev.fyloz.colorrecipesexplorer.logic.users.*
import org.junit.jupiter.api.*
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import java.util.*
import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletRequest
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserLogicTest :
AbstractExternalModelServiceTest<User, UserSaveDto, UserUpdateDto, UserLogic, UserRepository>() {
private val passwordEncoder = BCryptPasswordEncoder()
override val entity: User = user(id = 0L, passwordEncoder = passwordEncoder)
override val anotherEntity: User = user(id = 1L, passwordEncoder = passwordEncoder)
private val entityDefaultGroupUser = user(id = 2L, isDefaultGroupUser = true, passwordEncoder = passwordEncoder)
private val entitySystemUser = user(id = 3L, isSystemUser = true, passwordEncoder = passwordEncoder)
private val group = group(id = 0L)
override val entitySaveDto: UserSaveDto = spy(userSaveDto(passwordEncoder, id = 0L))
override val entityUpdateDto: UserUpdateDto = spy(userUpdateDto(id = 0L))
override val repository: UserRepository = mock()
private val groupService: GroupLogic = mock()
override val logic: UserLogic = spy(DefaultUserLogic(repository, groupService))
private val entitySaveDtoUser = User(
entitySaveDto.id,
entitySaveDto.firstName,
entitySaveDto.lastName,
passwordEncoder.encode(entitySaveDto.password),
isDefaultGroupUser = false,
isSystemUser = false,
group = null,
permissions = entitySaveDto.permissions
)
@AfterEach
override fun afterEach() {
reset(groupService)
super.afterEach()
}
// existsByFirstNameAndLastName()
@Test
fun `existsByFirstNameAndLastName() returns true when an user with the given first name and last name exists`() {
whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(true)
val found = logic.existsByFirstNameAndLastName(entity.firstName, entity.lastName)
assertTrue(found)
}
@Test
fun `existsByFirstNameAndLastName() returns false when no user with the given first name and last name exists`() {
whenever(repository.existsByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(false)
val found = logic.existsByFirstNameAndLastName(entity.firstName, entity.lastName)
assertFalse(found)
}
// getById()
@Test
fun `getById() throws NotFoundException when the corresponding user is a default group user`() {
whenever(repository.findById(entityDefaultGroupUser.id)).doReturn(Optional.of(entityDefaultGroupUser))
assertThrows<NotFoundException> {
logic.getById(
entityDefaultGroupUser.id,
ignoreDefaultGroupUsers = true,
ignoreSystemUsers = false
)
}.assertErrorCode()
}
@Test
fun `getById() throws NotFoundException when the corresponding user is a system user`() {
whenever(repository.findById(entitySystemUser.id)).doReturn(Optional.of(entitySystemUser))
assertThrows<NotFoundException> {
logic.getById(
entitySystemUser.id,
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
)
}.assertErrorCode()
}
// getByGroup()
@Test
fun `getByGroup() returns all the users with the given group from the repository`() {
whenever(repository.findAllByGroup(group)).doReturn(entityList)
val found = logic.getByGroup(group)
assertTrue(found.containsAll(entityList))
assertTrue(entityList.containsAll(found))
}
@Test
fun `getByGroup() returns an empty list when there is no user with the given group in the repository`() {
whenever(repository.findAllByGroup(group)).doReturn(listOf())
val found = logic.getByGroup(group)
assertTrue(found.isEmpty())
}
// getDefaultGroupUser()
@Test
fun `getDefaultGroupUser() returns the default user of the given group from the repository`() {
whenever(repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)).doReturn(entityDefaultGroupUser)
val found = logic.getDefaultGroupUser(group)
assertEquals(entityDefaultGroupUser, found)
}
// save()
override fun `save() saves in the repository and returns the saved value`() {
whenever(repository.save(entity)).doReturn(entity)
doReturn(false).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName)
val found = logic.save(entity)
verify(repository).save(entity)
assertEquals(entity, found)
}
@Test
fun `save() throws AlreadyExistsException when firstName and lastName exists`() {
doReturn(true).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName)
assertThrows<AlreadyExistsException> { logic.save(entity) }
.assertErrorCode("fullName")
}
@Test
override fun `save(dto) calls and returns save() with the created entity`() {
withBaseSaveDtoTest(entity, entitySaveDto, logic, {
argThat {
this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName
}
})
}
@Test
fun `save(dto) calls and returns save() with the created user`() {
doReturn(entitySaveDtoUser).whenever(logic).save(any<User>())
val found = logic.save(entitySaveDto)
verify(logic).save(argThat<User> { this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName })
assertEquals(entitySaveDtoUser, found)
}
// update()
@Test
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() })
@Test
fun `update() throws AlreadyExistsException when a different user with the given first name and last name exists`() {
whenever(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(
entityDefaultGroupUser
)
doReturn(entity).whenever(logic).getById(eq(entity.id), any(), any())
assertThrows<AlreadyExistsException> {
logic.update(
entity,
true,
ignoreSystemUsers = true
)
}.assertErrorCode("fullName")
}
}
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GroupLogicTest :
AbstractExternalNamedModelServiceTest<Group, GroupSaveDto, GroupUpdateDto, GroupLogic, GroupRepository>() {
private val userService: UserLogic = mock()
override val repository: GroupRepository = mock()
override val logic: DefaultGroupLogic = spy(DefaultGroupLogic(userService, repository))
override val entity: Group = group(id = 0L, name = "group")
override val anotherEntity: Group = group(id = 1L, name = "another group")
override val entitySaveDto: GroupSaveDto = spy(groupSaveDto(name = "group"))
override val entityUpdateDto: GroupUpdateDto = spy(groupUpdateDto(id = 0L, name = "group"))
override val entityWithEntityName: Group = group(id = 2L, name = entity.name)
private val groupUserId = 1000000L + entity.id!!
private val groupUser = user(passwordEncoder = BCryptPasswordEncoder(), id = groupUserId, group = entity)
@BeforeEach
override fun afterEach() {
reset(userService)
super.afterEach()
}
// getUsersForGroup()
@Test
fun `getUsersForGroup() returns all users in the given group`() {
val group = group(id = 1L)
doReturn(group).whenever(logic).getById(group.id!!)
whenever(userService.getByGroup(group)).doReturn(listOf(groupUser))
val found = logic.getUsersForGroup(group.id!!)
assertTrue(found.contains(groupUser))
assertTrue(found.size == 1)
}
@Test
fun `getUsersForGroup() returns empty collection when the given group contains any user`() {
doReturn(entity).whenever(logic).getById(entity.id!!)
val found = logic.getUsersForGroup(entity.id!!)
assertTrue(found.isEmpty())
}
// getRequestDefaultGroup()
@Test
fun `getRequestDefaultGroup() returns the group contained in the cookie of the HTTP request`() {
val cookies: Array<Cookie> = arrayOf(Cookie(defaultGroupCookieName, groupUserId.toString()))
val request: HttpServletRequest = mock()
whenever(request.cookies).doReturn(cookies)
whenever(userService.getById(eq(groupUserId), any(), any())).doReturn(groupUser)
val found = logic.getRequestDefaultGroup(request)
assertEquals(entity, found)
}
@Test
fun `getRequestDefaultGroup() throws NoDefaultGroupException when the HTTP request does not contains a cookie for the default group`() {
val request: HttpServletRequest = mock()
whenever(request.cookies).doReturn(arrayOf())
assertThrows<NoDefaultGroupException> { logic.getRequestDefaultGroup(request) }
}
// setResponseDefaultGroup()
@Test
fun `setResponseDefaultGroup() the default group cookie has been added to the given HTTP response with the given group id`() {
val response = MockHttpServletResponse()
whenever(userService.getDefaultGroupUser(entity)).doReturn(groupUser)
doReturn(entity).whenever(logic).getById(entity.id!!)
logic.setResponseDefaultGroup(entity.id!!, response)
val found = response.getCookie(defaultGroupCookieName)
assertNotNull(found)
assertEquals(defaultGroupCookieName, found.name)
assertEquals(groupUserId.toString(), found.value)
assertEquals(defaultGroupCookieMaxAge, found.maxAge)
assertTrue(found.isHttpOnly)
assertTrue(found.secure)
}
// save()
@Test
override fun `save(dto) calls and returns save() with the created entity`() {
withBaseSaveDtoTest(entity, entitySaveDto, logic)
}
// update()
@Test
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() })
}
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserUserDetailsLogicTest {
private val userLogic: UserLogic = mock()
private val logic = spy(DefaultUserDetailsLogic(userLogic))
private val user = user(id = 0L)
@BeforeEach
fun beforeEach() {
reset(userLogic, logic)
}
// loadUserByUsername()
@Test
fun `loadUserByUsername() calls loadUserByUserId() with the given username as an id`() {
whenever(userLogic.getById(eq(user.id), any(), any())).doReturn(user)
doReturn(UserDetails(user(id = user.id, plainPassword = user.password)))
.whenever(logic).loadUserById(user.id)
logic.loadUserByUsername(user.id.toString())
verify(logic).loadUserById(eq(user.id), any())
}
@Test
fun `loadUserByUsername() throws UsernameNotFoundException when no user with the given id exists`() {
whenever(userLogic.getById(eq(user.id), any(), any())).doThrow(
userIdNotFoundException(user.id)
)
assertThrows<UsernameNotFoundException> { logic.loadUserByUsername(user.id.toString()) }
}
// loadUserByUserId
@Test
fun `loadUserByUserId() returns an User corresponding to the user with the given id`() {
whenever(userLogic.getById(eq(user.id), any(), any())).doReturn(user)
val found = logic.loadUserById(user.id)
assertEquals(user.id, found.username.toLong())
assertEquals(user.password, found.password)
}
}

View File

@ -3,23 +3,23 @@ package dev.fyloz.colorrecipesexplorer.logic
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.fyloz.colorrecipesexplorer.config.properties.CreSecurityProperties
import dev.fyloz.colorrecipesexplorer.dtos.UserDetails
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.logic.users.DefaultJwtLogic
import dev.fyloz.colorrecipesexplorer.logic.users.jwtClaimUser
import dev.fyloz.colorrecipesexplorer.model.account.UserDetails
import dev.fyloz.colorrecipesexplorer.model.account.UserOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.toOutputDto
import dev.fyloz.colorrecipesexplorer.model.account.user
import dev.fyloz.colorrecipesexplorer.utils.base64encode
import dev.fyloz.colorrecipesexplorer.utils.isAround
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.jackson.io.JacksonDeserializer
import io.mockk.clearAllMocks
import io.mockk.spyk
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import java.time.Instant
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class JwtLogicTest {
class DefaultJwtLogicTest {
private val objectMapper = jacksonObjectMapper()
private val securityProperties = CreSecurityProperties().apply {
jwtSecret = "XRRm7OflmFuCrOB2Xvmfsercih9DCKom"
@ -34,12 +34,14 @@ class JwtLogicTest {
private val jwtService = spyk(DefaultJwtLogic(objectMapper, securityProperties))
private val user = user()
private val userOutputDto = user.toOutputDto()
private val user = UserDto(0L, "Unit test", "User", "", null, listOf())
// buildJwt()
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
private fun withParsedUserOutputDto(jwt: String, test: (UserOutputDto) -> Unit) {
private fun withParsedUserOutputDto(jwt: String, test: (UserDto) -> Unit) {
val serializedUser = jwtParser.parseClaimsJws(jwt)
.body.get(jwtClaimUser, String::class.java)
@ -47,27 +49,27 @@ class JwtLogicTest {
}
@Test
fun `buildJwt(userDetails) returns jwt string with valid user`() {
fun buildJwt_userDetails_normalBehavior_returnsJwtStringWithValidUser() {
val userDetails = UserDetails(user)
val builtJwt = jwtService.buildJwt(userDetails)
withParsedUserOutputDto(builtJwt) { parsedUser ->
assertEquals(user.toOutputDto(), parsedUser)
assertEquals(user, parsedUser)
}
}
@Test
fun `buildJwt() returns jwt string with valid user`() {
fun buildJwt_user_normalBehavior_returnsJwtStringWithValidUser() {
val builtJwt = jwtService.buildJwt(user)
withParsedUserOutputDto(builtJwt) { parsedUser ->
assertEquals(user.toOutputDto(), parsedUser)
assertEquals(user, parsedUser)
}
}
@Test
fun `buildJwt() returns jwt string with valid subject`() {
fun buildJwt_user_normalBehavior_returnsJwtStringWithValidSubject() {
val builtJwt = jwtService.buildJwt(user)
val jwtSubject = jwtParser.parseClaimsJws(builtJwt).body.subject
@ -75,7 +77,7 @@ class JwtLogicTest {
}
@Test
fun `buildJwt() returns jwt with valid expiration date`() {
fun buildJwt_user_returnsJwtWithValidExpirationDate() {
val jwtExpectedExpirationDate = Instant.now().plusSeconds(securityProperties.jwtDuration)
val builtJwt = jwtService.buildJwt(user)
@ -89,10 +91,10 @@ class JwtLogicTest {
// parseJwt()
@Test
fun `parseJwt() returns expected user`() {
fun parseJwt_normalBehavior_returnsExpectedUser() {
val jwt = jwtService.buildJwt(user)
val parsedUser = jwtService.parseJwt(jwt)
assertEquals(userOutputDto, parsedUser)
assertEquals(user, parsedUser)
}
}

View File

@ -3,7 +3,6 @@ package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.service.RecipeService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
@ -23,7 +22,7 @@ class DefaultRecipeLogicTest {
spyk(DefaultRecipeLogic(recipeServiceMock, companyLogicMock, recipeStepLogicMock, mixLogicMock, groupLogicMock))
private val company = CompanyDto(1L, "Unit test company")
private val group = Group(1L, "Unit test group")
private val group = GroupDto(1L, "Unit test group", listOf())
private val recipe = RecipeDto(
1L,
"Unit test recipe",
@ -160,7 +159,7 @@ class DefaultRecipeLogicTest {
val expectedGroupInformation = RecipeGroupInformationDto(0L, group, "Unit test note", listOf())
val groupNote = RecipeGroupNoteDto(group.id!!, expectedGroupInformation.note)
val groupNote = RecipeGroupNoteDto(group.id, expectedGroupInformation.note)
val dto = RecipePublicDataDto(recipe.id, listOf(groupNote), listOf())
// Act
@ -189,7 +188,7 @@ class DefaultRecipeLogicTest {
// Arrange
every { mixLogicMock.updateLocations(any()) } just runs
val mixesLocation = listOf(MixLocationDto(group.id!!, "location"))
val mixesLocation = listOf(MixLocationDto(group.id, "location"))
val dto = RecipePublicDataDto(recipe.id, listOf(), mixesLocation)
// Act

View File

@ -1,10 +1,10 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.service.RecipeStepService
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
import io.mockk.*
@ -28,7 +28,7 @@ class DefaultRecipeStepLogicTest {
mockkObject(PositionUtils)
every { PositionUtils.validate(any()) } just runs
val group = Group(1L, "Unit test group")
val group = GroupDto(1L, "Unit test group", listOf())
val steps = listOf(RecipeStepDto(1L, 1, "A message"))
val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps)
@ -49,7 +49,7 @@ class DefaultRecipeStepLogicTest {
mockkObject(PositionUtils)
every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors)
val group = Group(1L, "Unit test group")
val group = GroupDto(1L, "Unit test group", listOf())
val steps = listOf(RecipeStepDto(1L, 1, "A message"))
val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps)

View File

@ -1,138 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
import dev.fyloz.colorrecipesexplorer.model.configuration
import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository
import dev.fyloz.colorrecipesexplorer.utils.PdfDocument
import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.springframework.core.io.ByteArrayResource
import kotlin.test.assertEquals
//private class TouchUpKitServiceTestContext {
// val touchUpKitRepository = mockk<TouchUpKitRepository>()
// val fileService = mockk<WriteableFileLogic> {
// every { write(any<ByteArrayResource>(), any(), any()) } just Runs
// }
// val configService = mockk<ConfigurationLogic>(relaxed = true)
// val touchUpKitService = spyk(DefaultTouchUpKitLogic(fileService, configService, touchUpKitRepository))
// val pdfDocumentData = mockk<ByteArrayResource>()
// val pdfDocument = mockk<PdfDocument> {
// mockkStatic(PdfDocument::toByteArrayResource)
// mockkStatic(PdfDocument::toByteArrayResource)
// every { toByteArrayResource() } returns pdfDocumentData
// }
//}
class TouchUpKitLogicTest {
// private val job = "job"
//
// @AfterEach
// internal fun afterEach() {
// clearAllMocks()
// }
//
// // generateJobPdf()
//
// @Test
// fun `generateJobPdf() generates a valid PdfDocument for the given job`() {
// test {
// val generatedPdfDocument = touchUpKitService.generateJobPdf(job)
//
// setOf(0, 1).forEach {
// assertEquals(TOUCH_UP_TEXT_FR, generatedPdfDocument.containers[it].texts[0].text)
// assertEquals(TOUCH_UP_TEXT_EN, generatedPdfDocument.containers[it].texts[1].text)
// assertEquals(job, generatedPdfDocument.containers[it].texts[2].text)
// }
// }
// }
//
// // generateJobPdfResource()
//
// @Test
// fun `generateJobPdfResource() generates and returns a ByteArrayResource for the given job then cache it`() {
// test {
// every { touchUpKitService.generateJobPdf(any()) } returns pdfDocument
// with(touchUpKitService) {
// every { job.cachePdfDocument(pdfDocument) } just Runs
// }
//
// val generatedResource = touchUpKitService.generateJobPdfResource(job)
//
// assertEquals(pdfDocumentData, generatedResource)
//
// verify {
// with(touchUpKitService) {
// job.cachePdfDocument(pdfDocument)
// }
// }
// }
// }
//
// @Test
// fun `generateJobPdfResource() returns a cached ByteArrayResource from the FileService when caching is enabled and a cached file eixsts for the given job`() {
// test {
// enableCachePdf()
// every { fileService.exists(any()) } returns true
// every { fileService.read(any()) } returns pdfDocumentData
// every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration(
// ConfigurationType.TOUCH_UP_KIT_CACHE_PDF,
// "true"
// )
//
// val redResource = touchUpKitService.generateJobPdfResource(job)
//
// assertEquals(pdfDocumentData, redResource)
// }
// }
//
// // String.cachePdfDocument()
//
// @Test
// fun `cachePdfDocument() does nothing when caching is disabled`() {
// test {
// disableCachePdf()
//
// with(touchUpKitService) {
// job.cachePdfDocument(pdfDocument)
// }
//
// verify(exactly = 0) {
// fileService.write(any<ByteArrayResource>(), any(), any())
// }
// }
// }
//
// @Test
// fun `cachePdfDocument() writes the given document to the FileService when cache is enabled`() {
// test {
// enableCachePdf()
//
// with(touchUpKitService) {
// job.cachePdfDocument(pdfDocument)
// }
//
// verify {
// fileService.write(pdfDocumentData, any(), true)
// }
// }
// }
//
// private fun TouchUpKitServiceTestContext.enableCachePdf() =
// this.setCachePdf(true)
//
// private fun TouchUpKitServiceTestContext.disableCachePdf() =
// this.setCachePdf(false)
//
// private fun TouchUpKitServiceTestContext.setCachePdf(enabled: Boolean) {
// every { configService.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns enabled.toString()
// }
//
// private fun test(test: TouchUpKitServiceTestContext.() -> Unit) {
// TouchUpKitServiceTestContext().test()
// }
}

View File

@ -0,0 +1,87 @@
package dev.fyloz.colorrecipesexplorer.logic.account
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.logic.users.DefaultGroupLogic
import dev.fyloz.colorrecipesexplorer.logic.users.UserLogic
import dev.fyloz.colorrecipesexplorer.service.GroupService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
class DefaultGroupLogicTest {
private val group = GroupDto(1L, "Unit test group", listOf())
private val user = UserDto(1L, "Unit test", "User", "asecurepassword", null, listOf())
private val groupServiceMock = mockk<GroupService> {
every { existsById(any()) } returns false
every { existsByName(any(), any()) } returns false
every { getAll() } returns listOf()
every { getById(any()) } returns group
every { save(any()) } returns group
every { deleteById(any()) } just runs
}
private val userLogicMock = mockk<UserLogic> {
every { getAllByGroup(any()) } returns listOf()
every { getById(any(), any(), any()) } returns user
every { getDefaultGroupUser(any()) } returns user
every { saveDefaultGroupUser(any()) } just runs
every { deleteById(any()) } just runs
}
private val groupLogic = spyk(DefaultGroupLogic(groupServiceMock, userLogicMock))
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
@Test
fun getUsersForGroup_normalBehavior_callsGetAllByGroupInUserLogic() {
// Arrange
every { groupLogic.getById(any()) } returns group
// Act
groupLogic.getUsersForGroup(group.id)
// Assert
verify {
userLogicMock.getAllByGroup(group)
}
confirmVerified(userLogicMock)
}
@Test
fun save_nameAlreadyExists_throwsAlreadyExists() {
// Arrange
every { groupServiceMock.existsByName(any(), any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { groupLogic.save(group) }
}
@Test
fun update_normalBehavior_throwsAlreadyExists() {
// Arrange
every { groupServiceMock.existsByName(any(), any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { groupLogic.update(group) }
}
@Test
fun deleteById_normalBehavior_callsDeleteByIdInUserLogicWithDefaultGroupUserId() {
// Arrange
// Act
groupLogic.deleteById(group.id)
// Assert
verify {
userLogicMock.deleteById(group.defaultGroupUserId)
}
}
}

View File

@ -0,0 +1,306 @@
package dev.fyloz.colorrecipesexplorer.logic.account
import dev.fyloz.colorrecipesexplorer.dtos.GroupDto
import dev.fyloz.colorrecipesexplorer.dtos.UserDto
import dev.fyloz.colorrecipesexplorer.dtos.UserSaveDto
import dev.fyloz.colorrecipesexplorer.dtos.UserUpdateDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.logic.users.DefaultUserLogic
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
import dev.fyloz.colorrecipesexplorer.model.account.Permission
import dev.fyloz.colorrecipesexplorer.service.UserService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
class DefaultUserLogicTest {
private val user = UserDto(1L, "Unit test", "User", "asecurepassword", null, listOf())
private val group = GroupDto(1L, "Unit test group", listOf())
private val userServiceMock = mockk<UserService> {
every { existsById(any()) } returns false
every { existsByFirstNameAndLastName(any(), any(), any()) } returns false
every { getAll(any(), any()) } returns listOf()
every { getAllByGroup(any()) } returns listOf()
every { getById(any(), any(), any()) } returns user
every { getByFirstNameAndLastName(any(), any()) } returns user
every { getDefaultGroupUser(any()) } returns user
}
private val groupLogicMock = mockk<GroupLogic> {
every { getById(any()) } returns group
}
private val passwordEncoderMock = mockk<PasswordEncoder> {
every { encode(any()) } answers { "encoded ${this.firstArg<String>()}" }
}
private val userLogic = spyk(DefaultUserLogic(userServiceMock, groupLogicMock, passwordEncoderMock))
private val userSaveDto = UserSaveDto(
user.id,
user.firstName,
user.lastName,
user.password,
null,
user.permissions,
user.isSystemUser,
user.isDefaultGroupUser
)
private val userUpdateDto = UserUpdateDto(user.id, user.firstName, user.lastName, null, listOf())
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
@Test
fun getAll_normalBehavior_callsGetAllInServiceWithSpecialUsersDisabled() {
// Arrange
// Act
userLogic.getAll()
// Assert
verify {
userServiceMock.getAll(isSystemUser = false, isDefaultGroupUser = false)
}
confirmVerified(userServiceMock)
}
@Test
fun getAllByGroup_normalBehavior_callsGetAllByGroupInService() {
// Arrange
// Act
userLogic.getAllByGroup(group)
// Assert
verify {
userServiceMock.getAllByGroup(group)
}
confirmVerified(userServiceMock)
}
@Test
fun getById_default_normalBehavior_callsGetByIdWithSpecialUsersDisabled() {
// Arrange
// Act
userLogic.getById(user.id)
// Assert
verify {
userLogic.getById(user.id, isSystemUser = false, isDefaultGroupUser = false)
}
}
@Test
fun getById_normalBehavior_callsGetByIdInService() {
// Arrange
// Act
userLogic.getById(user.id, isSystemUser = false, isDefaultGroupUser = true)
// Assert
verify {
userServiceMock.getById(user.id, isSystemUser = false, isDefaultGroupUser = true)
}
confirmVerified(userServiceMock)
}
@Test
fun getById_notFound_throwsNotFoundException() {
// Arrange
every { userServiceMock.getById(any(), any(), any()) } returns null
// Act
// Assert
assertThrows<NotFoundException> { userLogic.getById(user.id) }
}
@Test
fun getDefaultGroupUser_normalBehavior_callsGetDefaultGroupUserInService() {
// Arrange
// Act
userLogic.getDefaultGroupUser(group)
// Assert
verify {
userServiceMock.getDefaultGroupUser(group)
}
confirmVerified(userServiceMock)
}
@Test
fun getDefaultGroupUser_notFound_throwsNotFoundException() {
// Arrange
every { userServiceMock.getDefaultGroupUser(any()) } returns null
// Act
// Assert
assertThrows<NotFoundException> { userLogic.getDefaultGroupUser(group) }
}
@Test
fun saveDefaultGroupUser_normalBehavior_callsSaveWithValidSaveDto() {
// Arrange
every { userLogic.save(any<UserSaveDto>()) } returns user
val expectedSaveDto = UserSaveDto(
group.defaultGroupUserId, group.name, "User", group.name, group.id, listOf(), isDefaultGroupUser = true
)
// Act
userLogic.saveDefaultGroupUser(group)
// Assert
verify {
userLogic.save(expectedSaveDto)
}
}
@Test
fun save_dto_normalBehavior_callsSaveWithValidUser() {
// Arrange
every { userLogic.save(any<UserDto>()) } returns user
val expectedUser = user.copy(password = "encoded ${user.password}")
// Act
userLogic.save(userSaveDto)
// Assert
verify {
userLogic.save(expectedUser)
}
}
// TODO Causes a stackoverflow because of a bug in mockk
// @Test
// fun save_normalBehavior_callsSaveInService() {
// // Arrange
// // Act
// userLogic.save(user)
//
// // Assert
// verify {
// userServiceMock.save(user)
// }
// }
@Test
fun save_idAlreadyExists_throwsAlreadyExistsException() {
// Arrange
every { userServiceMock.existsById(any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { userLogic.save(user) }
}
@Test
fun save_fullNameAlreadyExists_throwsAlreadyExistsException() {
// Arrange
every { userServiceMock.existsByFirstNameAndLastName(any(), any(), any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { userLogic.save(userSaveDto) }
}
@Test
fun update_dto_normalBehavior_callsUpdateWithValidUser() {
// Arrange
every { userLogic.getById(any(), any(), any()) } returns user
every { userLogic.update(any<UserDto>()) } returns user
// Act
userLogic.update(userUpdateDto)
// Assert
verify {
userLogic.update(user)
}
}
@Test
fun update_fullNameAlreadyExists_ThrowAlreadyExistsException() {
// Arrange
every { userServiceMock.existsByFirstNameAndLastName(any(), any(), any()) } returns true
// Act
// Assert
assertThrows<AlreadyExistsException> { userLogic.update(user) }
}
@Test
fun updateLastLoginTime_normalBehavior_callsUpdateWithUpdatedTime() {
// Arrange
every { userLogic.getById(any()) } returns user
every { userLogic.update(any<UserDto>()) } returns user
val time = LocalDateTime.now()
val expectedUser = user.copy(lastLoginTime = time)
// Act
userLogic.updateLastLoginTime(user.id, time)
// Assert
verify {
userLogic.update(expectedUser)
}
}
@Test
fun updatePassword_normalBehavior_callsUpdateWithUpdatedTime() {
// Arrange
every { userLogic.getById(any()) } returns user
every { userLogic.update(any<UserDto>()) } returns user
val updatedPassword = "updatedpassword"
val expectedUser = user.copy(password = "encoded $updatedPassword")
// Act
userLogic.updatePassword(user.id, updatedPassword)
// Assert
verify {
userLogic.update(expectedUser)
}
}
@Test
fun addPermission_normalBehavior_callsUpdateWithAddedPermission() {
// Arrange
every { userLogic.getById(any()) } returns user
every { userLogic.update(any<UserDto>()) } returns user
val addedPermission = Permission.VIEW_COMPANY
val expectedUser = user.copy(permissions = user.permissions + addedPermission)
// Act
userLogic.addPermission(user.id, addedPermission)
// Assert
verify {
userLogic.update(expectedUser)
}
}
@Test
fun removePermission_normalBehavior_callsUpdateWithAddedPermission() {
// Arrange
val removedPermission = Permission.VIEW_COMPANY
val baseUser = user.copy(permissions = user.permissions + removedPermission)
every { userLogic.getById(any()) } returns baseUser
every { userLogic.update(any<UserDto>()) } returns user
// Act
userLogic.removePermission(user.id, removedPermission)
// Assert
verify {
userLogic.update(user)
}
}
}

View File

@ -21,7 +21,7 @@ private const val mockFilePath = "existingFile"
private val mockFilePathPath = Path.of(mockFilePath)
private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf)
class FileLogicTest {
class DefaultFileLogicTest {
private val fileCacheMock = mockk<FileCache> {
every { setExists(any(), any()) } just runs
}