mirror of
https://github.com/gotson/komga.git
synced 2025-12-06 16:42:24 +01:00
feat(api): user creation supports restrictions
This commit is contained in:
parent
e2a0b9450e
commit
1f0817bbe7
6 changed files with 186 additions and 30 deletions
|
|
@ -7,6 +7,7 @@ import jakarta.validation.Valid
|
|||
import org.gotson.komga.domain.model.AgeRestriction
|
||||
import org.gotson.komga.domain.model.ContentRestrictions
|
||||
import org.gotson.komga.domain.model.DuplicateNameException
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.UserEmailAlreadyExistsException
|
||||
import org.gotson.komga.domain.model.UserRoles
|
||||
import org.gotson.komga.domain.persistence.AuthenticationActivityRepository
|
||||
|
|
@ -16,6 +17,7 @@ import org.gotson.komga.domain.service.KomgaUserLifecycle
|
|||
import org.gotson.komga.infrastructure.jooq.UnpagedSorted
|
||||
import org.gotson.komga.infrastructure.openapi.OpenApiConfiguration.TagNames
|
||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
import org.gotson.komga.interfaces.api.rest.dto.AllowExcludeDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.ApiKeyDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.ApiKeyRequestDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.AuthenticationActivityDto
|
||||
|
|
@ -95,8 +97,35 @@ class UserController(
|
|||
newUser: UserCreationDto,
|
||||
): UserDto =
|
||||
try {
|
||||
userLifecycle.createUser(newUser.toDomain()).toDto()
|
||||
} catch (e: UserEmailAlreadyExistsException) {
|
||||
userLifecycle
|
||||
.createUser(
|
||||
with(newUser) {
|
||||
KomgaUser(
|
||||
email,
|
||||
password,
|
||||
roles = UserRoles.Companion.valuesOf(roles),
|
||||
// keep existing behaviour before those properties were added, by default new user has access to all libraries
|
||||
sharedAllLibraries = sharedLibraries == null || sharedLibraries.all,
|
||||
sharedLibrariesIds =
|
||||
if (sharedLibraries == null || sharedLibraries.all)
|
||||
emptySet()
|
||||
else
|
||||
libraryRepository.findAllByIds(sharedLibraries.libraryIds).map { it.id }.toSet(),
|
||||
// keep existing behaviour before those properties were added, by default no restrictions are applied
|
||||
restrictions =
|
||||
ContentRestrictions(
|
||||
ageRestriction =
|
||||
if (ageRestriction == null || ageRestriction.restriction == AllowExcludeDto.NONE)
|
||||
null
|
||||
else
|
||||
AgeRestriction(ageRestriction.age, ageRestriction.restriction.toDomain()),
|
||||
labelsAllow = labelsAllow ?: emptySet(),
|
||||
labelsExclude = labelsExclude ?: emptySet(),
|
||||
),
|
||||
)
|
||||
},
|
||||
).toDto()
|
||||
} catch (_: UserEmailAlreadyExistsException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "A user with this email already exists")
|
||||
}
|
||||
|
||||
|
|
@ -142,10 +171,10 @@ class UserController(
|
|||
ContentRestrictions(
|
||||
ageRestriction =
|
||||
if (isSet("ageRestriction")) {
|
||||
if (ageRestriction == null)
|
||||
if (ageRestriction == null || ageRestriction?.restriction == AllowExcludeDto.NONE)
|
||||
null
|
||||
else
|
||||
AgeRestriction(ageRestriction!!.age, ageRestriction!!.restriction)
|
||||
AgeRestriction(ageRestriction!!.age, ageRestriction!!.restriction.toDomain())
|
||||
} else {
|
||||
existing.restrictions.ageRestriction
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package org.gotson.komga.interfaces.api.rest.dto
|
||||
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
|
||||
data class PasswordUpdateDto(
|
||||
@get:NotBlank val password: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package org.gotson.komga.interfaces.api.rest.dto
|
||||
|
||||
import jakarta.validation.Valid
|
||||
import jakarta.validation.constraints.Email
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
|
||||
data class UserCreationDto(
|
||||
@get:Email(regexp = ".+@.+\\..+") val email: String,
|
||||
@get:NotBlank val password: String,
|
||||
val roles: List<String> = emptyList(),
|
||||
// new fields supported
|
||||
@get:Valid val ageRestriction: AgeRestrictionUpdateDto?,
|
||||
val labelsAllow: Set<String>?,
|
||||
val labelsExclude: Set<String>?,
|
||||
val sharedLibraries: SharedLibrariesUpdateDto?,
|
||||
)
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
package org.gotson.komga.interfaces.api.rest.dto
|
||||
|
||||
import jakarta.validation.constraints.Email
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import org.gotson.komga.domain.model.AgeRestriction
|
||||
import org.gotson.komga.domain.model.AllowExclude
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.UserRoles
|
||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
|
||||
data class UserDto(
|
||||
|
|
@ -39,20 +36,3 @@ fun KomgaUser.toDto() =
|
|||
)
|
||||
|
||||
fun KomgaPrincipal.toDto() = user.toDto()
|
||||
|
||||
data class UserCreationDto(
|
||||
@get:Email(regexp = ".+@.+\\..+") val email: String,
|
||||
@get:NotBlank val password: String,
|
||||
val roles: List<String> = emptyList(),
|
||||
) {
|
||||
fun toDomain(): KomgaUser =
|
||||
KomgaUser(
|
||||
email,
|
||||
password,
|
||||
roles = UserRoles.valuesOf(roles),
|
||||
)
|
||||
}
|
||||
|
||||
data class PasswordUpdateDto(
|
||||
@get:NotBlank val password: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,10 +40,24 @@ class UserUpdateDto {
|
|||
data class AgeRestrictionUpdateDto(
|
||||
@get:PositiveOrZero
|
||||
val age: Int,
|
||||
val restriction: AllowExclude,
|
||||
val restriction: AllowExcludeDto,
|
||||
)
|
||||
|
||||
data class SharedLibrariesUpdateDto(
|
||||
val all: Boolean,
|
||||
val libraryIds: Set<String>,
|
||||
)
|
||||
|
||||
enum class AllowExcludeDto {
|
||||
ALLOW_ONLY,
|
||||
EXCLUDE,
|
||||
NONE,
|
||||
;
|
||||
|
||||
fun toDomain() =
|
||||
when (this) {
|
||||
ALLOW_ONLY -> AllowExclude.ALLOW_ONLY
|
||||
EXCLUDE -> AllowExclude.EXCLUDE
|
||||
NONE -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import org.gotson.komga.domain.persistence.KomgaUserRepository
|
|||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.service.KomgaUserLifecycle
|
||||
import org.gotson.komga.domain.service.LibraryLifecycle
|
||||
import org.gotson.komga.interfaces.api.rest.dto.AllowExcludeDto
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.hamcrest.Matchers.hasItem
|
||||
import org.hamcrest.text.MatchesPattern
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
|
|
@ -69,19 +72,126 @@ class UserControllerTest(
|
|||
.forEach { userLifecycle.deleteUser(it) }
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = ["user", "user@domain"])
|
||||
@Nested
|
||||
inner class Create {
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = ["user", "user@domain"])
|
||||
@WithMockCustomUser(roles = ["ADMIN"])
|
||||
fun `when creating a user with invalid email then returns bad request`(email: String) {
|
||||
// language=JSON
|
||||
val jsonString = """{"email":"$email","password":"password"}"""
|
||||
|
||||
mockMvc
|
||||
.post("/api/v2/users") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isBadRequest() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(roles = ["ADMIN"])
|
||||
fun `when creating a user with limited fields then returns created user`() {
|
||||
// language=JSON
|
||||
val jsonString =
|
||||
"""
|
||||
{
|
||||
"email":"newuser@example.org",
|
||||
"password":"password"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
mockMvc
|
||||
.post("/api/v2/users") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isCreated() }
|
||||
jsonPath("$.roles") { value(hasItem("USER")) }
|
||||
jsonPath("$.sharedAllLibraries") { value(true) }
|
||||
jsonPath("$.sharedLibrariesId") { doesNotExist() }
|
||||
jsonPath("$.labelsAllow") { isEmpty() }
|
||||
jsonPath("$.labelsExclude") { isEmpty() }
|
||||
jsonPath("$.ageRestriction") { isEmpty() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(roles = ["ADMIN"])
|
||||
fun `when creating a user with invalid email then returns bad request`(email: String) {
|
||||
fun `when creating a user with all fields then returns created user`() {
|
||||
// language=JSON
|
||||
val jsonString = """{"email":"$email","password":"password"}"""
|
||||
val jsonString =
|
||||
"""
|
||||
{
|
||||
"email":"newuser@example.org",
|
||||
"password":"password",
|
||||
"roles": ["${UserRoles.FILE_DOWNLOAD.name}"],
|
||||
"ageRestriction": {
|
||||
"age": 5,
|
||||
"restriction": "${AllowExcludeDto.NONE}"
|
||||
},
|
||||
"labelsAllow": ["allowTag"],
|
||||
"labelsExclude": ["excludeTag"],
|
||||
"sharedLibraries": {
|
||||
"all": "false",
|
||||
"libraryIds" : ["1", "157"]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
mockMvc
|
||||
.post("/api/v2/users") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isBadRequest() }
|
||||
status { isCreated() }
|
||||
jsonPath("$.roles") { value(allOf(hasItem("USER"), hasItem(UserRoles.FILE_DOWNLOAD.name))) }
|
||||
jsonPath("$.labelsAllow") { value(hasItem("allowtag")) }
|
||||
jsonPath("$.labelsExclude") { value(hasItem("excludetag")) }
|
||||
jsonPath("$.ageRestriction") { doesNotExist() }
|
||||
jsonPath("$.sharedAllLibraries") { value(false) }
|
||||
jsonPath("$.sharedLibrariesIds") { value(hasItem("1")) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(roles = ["ADMIN"])
|
||||
fun `when creating a user with some fields then returns created user`() {
|
||||
// language=JSON
|
||||
val jsonString =
|
||||
"""
|
||||
{
|
||||
"email":"newuser@example.org",
|
||||
"password":"password",
|
||||
"roles": ["${UserRoles.FILE_DOWNLOAD.name}"],
|
||||
"ageRestriction": {
|
||||
"age": 5,
|
||||
"restriction": "${AllowExcludeDto.ALLOW_ONLY}"
|
||||
},
|
||||
"labelsAllow": ["allowTag"],
|
||||
"labelsExclude": ["excludeTag"],
|
||||
"sharedLibraries": {
|
||||
"all": "false",
|
||||
"libraryIds" : ["1", "157"]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
mockMvc
|
||||
.post("/api/v2/users") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isCreated() }
|
||||
jsonPath("$.roles") { value(allOf(hasItem("USER"), hasItem(UserRoles.FILE_DOWNLOAD.name))) }
|
||||
jsonPath("$.labelsAllow") { value(hasItem("allowtag")) }
|
||||
jsonPath("$.labelsExclude") { value(hasItem("excludetag")) }
|
||||
jsonPath("$.ageRestriction.age") { value(5) }
|
||||
jsonPath("$.ageRestriction.restriction") { value(AllowExclude.ALLOW_ONLY.name) }
|
||||
jsonPath("$.sharedAllLibraries") { value(false) }
|
||||
jsonPath("$.sharedLibrariesIds") { value(hasItem("1")) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue