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.AgeRestriction
|
||||||
import org.gotson.komga.domain.model.ContentRestrictions
|
import org.gotson.komga.domain.model.ContentRestrictions
|
||||||
import org.gotson.komga.domain.model.DuplicateNameException
|
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.UserEmailAlreadyExistsException
|
||||||
import org.gotson.komga.domain.model.UserRoles
|
import org.gotson.komga.domain.model.UserRoles
|
||||||
import org.gotson.komga.domain.persistence.AuthenticationActivityRepository
|
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.jooq.UnpagedSorted
|
||||||
import org.gotson.komga.infrastructure.openapi.OpenApiConfiguration.TagNames
|
import org.gotson.komga.infrastructure.openapi.OpenApiConfiguration.TagNames
|
||||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
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.ApiKeyDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.ApiKeyRequestDto
|
import org.gotson.komga.interfaces.api.rest.dto.ApiKeyRequestDto
|
||||||
import org.gotson.komga.interfaces.api.rest.dto.AuthenticationActivityDto
|
import org.gotson.komga.interfaces.api.rest.dto.AuthenticationActivityDto
|
||||||
|
|
@ -95,8 +97,35 @@ class UserController(
|
||||||
newUser: UserCreationDto,
|
newUser: UserCreationDto,
|
||||||
): UserDto =
|
): UserDto =
|
||||||
try {
|
try {
|
||||||
userLifecycle.createUser(newUser.toDomain()).toDto()
|
userLifecycle
|
||||||
} catch (e: UserEmailAlreadyExistsException) {
|
.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")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "A user with this email already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,10 +171,10 @@ class UserController(
|
||||||
ContentRestrictions(
|
ContentRestrictions(
|
||||||
ageRestriction =
|
ageRestriction =
|
||||||
if (isSet("ageRestriction")) {
|
if (isSet("ageRestriction")) {
|
||||||
if (ageRestriction == null)
|
if (ageRestriction == null || ageRestriction?.restriction == AllowExcludeDto.NONE)
|
||||||
null
|
null
|
||||||
else
|
else
|
||||||
AgeRestriction(ageRestriction!!.age, ageRestriction!!.restriction)
|
AgeRestriction(ageRestriction!!.age, ageRestriction!!.restriction.toDomain())
|
||||||
} else {
|
} else {
|
||||||
existing.restrictions.ageRestriction
|
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
|
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.AgeRestriction
|
||||||
import org.gotson.komga.domain.model.AllowExclude
|
import org.gotson.komga.domain.model.AllowExclude
|
||||||
import org.gotson.komga.domain.model.KomgaUser
|
import org.gotson.komga.domain.model.KomgaUser
|
||||||
import org.gotson.komga.domain.model.UserRoles
|
|
||||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||||
|
|
||||||
data class UserDto(
|
data class UserDto(
|
||||||
|
|
@ -39,20 +36,3 @@ fun KomgaUser.toDto() =
|
||||||
)
|
)
|
||||||
|
|
||||||
fun KomgaPrincipal.toDto() = user.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(
|
data class AgeRestrictionUpdateDto(
|
||||||
@get:PositiveOrZero
|
@get:PositiveOrZero
|
||||||
val age: Int,
|
val age: Int,
|
||||||
val restriction: AllowExclude,
|
val restriction: AllowExcludeDto,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SharedLibrariesUpdateDto(
|
data class SharedLibrariesUpdateDto(
|
||||||
val all: Boolean,
|
val all: Boolean,
|
||||||
val libraryIds: Set<String>,
|
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.persistence.LibraryRepository
|
||||||
import org.gotson.komga.domain.service.KomgaUserLifecycle
|
import org.gotson.komga.domain.service.KomgaUserLifecycle
|
||||||
import org.gotson.komga.domain.service.LibraryLifecycle
|
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.hamcrest.text.MatchesPattern
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
|
@ -69,19 +72,126 @@ class UserControllerTest(
|
||||||
.forEach { userLifecycle.deleteUser(it) }
|
.forEach { userLifecycle.deleteUser(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@Nested
|
||||||
@ValueSource(strings = ["user", "user@domain"])
|
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"])
|
@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
|
// 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
|
mockMvc
|
||||||
.post("/api/v2/users") {
|
.post("/api/v2/users") {
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
content = jsonString
|
content = jsonString
|
||||||
}.andExpect {
|
}.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