mirror of
https://github.com/gotson/komga.git
synced 2025-12-16 13:33:49 +01:00
feat(api): add /api/v2/users and deprecate /api/v1/users
simplify the user DTO classes make api logout endpoint to version agnostic
This commit is contained in:
parent
f1ab136b5e
commit
fa04d9511a
14 changed files with 508 additions and 71 deletions
|
|
@ -142,7 +142,7 @@ export default Vue.extend({
|
|||
modalDeleteUser: false,
|
||||
userToDelete: {} as UserDto,
|
||||
modalEditSharedLibraries: false,
|
||||
userToEditSharedLibraries: {} as UserWithSharedLibrariesDto,
|
||||
userToEditSharedLibraries: {} as UserDto,
|
||||
modalEditUser: false,
|
||||
userToEdit: {} as UserDto,
|
||||
modalChangePassword: false,
|
||||
|
|
@ -150,7 +150,7 @@ export default Vue.extend({
|
|||
usersLastActivity: {} as any,
|
||||
}),
|
||||
computed: {
|
||||
users(): UserWithSharedLibrariesDto[] {
|
||||
users(): UserDto[] {
|
||||
return this.$store.state.komgaUsers.users
|
||||
},
|
||||
me(): UserDto {
|
||||
|
|
@ -175,7 +175,7 @@ export default Vue.extend({
|
|||
this.userToDelete = user
|
||||
this.modalDeleteUser = true
|
||||
},
|
||||
editSharedLibraries(user: UserWithSharedLibrariesDto) {
|
||||
editSharedLibraries(user: UserDto) {
|
||||
this.userToEditSharedLibraries = user
|
||||
this.modalEditSharedLibraries = true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -104,11 +104,11 @@ export default Vue.extend({
|
|||
},
|
||||
async editUser() {
|
||||
try {
|
||||
const roles = {
|
||||
const patch = {
|
||||
roles: this.roles,
|
||||
} as RolesUpdateDto
|
||||
} as UserUpdateDto
|
||||
|
||||
await this.$store.dispatch('updateUserRoles', {userId: this.user.id, roles: roles})
|
||||
await this.$store.dispatch('updateUser', {userId: this.user.id, patch: patch})
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,12 +91,12 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
dialogReset(user: UserWithSharedLibrariesDto) {
|
||||
dialogReset(user: UserDto) {
|
||||
this.allLibraries = user.sharedAllLibraries
|
||||
if (user.sharedAllLibraries) {
|
||||
this.selectedLibraries = this.libraries.map(x => x.id)
|
||||
} else {
|
||||
this.selectedLibraries = user.sharedLibraries.map(x => x.id)
|
||||
this.selectedLibraries = user.sharedLibrariesIds
|
||||
}
|
||||
},
|
||||
dialogCancel() {
|
||||
|
|
@ -110,12 +110,14 @@ export default Vue.extend({
|
|||
},
|
||||
async editUser() {
|
||||
try {
|
||||
const sharedLibraries = {
|
||||
all: this.allLibraries,
|
||||
libraryIds: this.selectedLibraries,
|
||||
} as SharedLibrariesUpdateDto
|
||||
const patch = {
|
||||
sharedLibraries: {
|
||||
all: this.allLibraries,
|
||||
libraryIds: this.selectedLibraries,
|
||||
},
|
||||
} as UserUpdateDto
|
||||
|
||||
await this.$store.dispatch('updateUserSharedLibraries', {user: this.user, sharedLibraries: sharedLibraries})
|
||||
await this.$store.dispatch('updateUser', {userId: this.user.id, patch: patch})
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ let service: KomgaUsersService
|
|||
const vuexModule: Module<any, any> = {
|
||||
state: {
|
||||
me: {} as UserDto,
|
||||
users: [] as UserWithSharedLibrariesDto[],
|
||||
users: [] as UserDto[],
|
||||
},
|
||||
getters: {
|
||||
meAdmin: state => state.me.hasOwnProperty('roles') && state.me.roles.includes(UserRoles.ADMIN),
|
||||
|
|
@ -21,7 +21,7 @@ const vuexModule: Module<any, any> = {
|
|||
setMe (state, user: UserDto) {
|
||||
state.me = user
|
||||
},
|
||||
setAllUsers (state, users: UserWithSharedLibrariesDto[]) {
|
||||
setAllUsers (state, users: UserDto[]) {
|
||||
state.users = users
|
||||
},
|
||||
},
|
||||
|
|
@ -46,18 +46,14 @@ const vuexModule: Module<any, any> = {
|
|||
await service.postUser(user)
|
||||
dispatch('getAllUsers')
|
||||
},
|
||||
async updateUserRoles ({ dispatch }, { userId, roles }: { userId: string, roles: RolesUpdateDto }) {
|
||||
await service.patchUserRoles(userId, roles)
|
||||
async updateUser ({ dispatch }, { userId, patch }: { userId: string, patch: UserUpdateDto }) {
|
||||
await service.patchUser(userId, patch)
|
||||
dispatch('getAllUsers')
|
||||
},
|
||||
async deleteUser ({ dispatch }, user: UserDto) {
|
||||
await service.deleteUser(user)
|
||||
dispatch('getAllUsers')
|
||||
},
|
||||
async updateUserSharedLibraries ({ dispatch }, { user, sharedLibraries }: { user: UserDto, sharedLibraries: SharedLibrariesUpdateDto }) {
|
||||
await service.patchUserSharedLibraries(user, sharedLibraries)
|
||||
dispatch('getAllUsers')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {AxiosInstance} from 'axios'
|
|||
|
||||
const qs = require('qs')
|
||||
|
||||
const API_USERS = '/api/v1/users'
|
||||
const API_USERS = '/api/v2/users'
|
||||
|
||||
export default class KomgaUsersService {
|
||||
private http: AxiosInstance
|
||||
|
|
@ -50,7 +50,7 @@ export default class KomgaUsersService {
|
|||
}
|
||||
}
|
||||
|
||||
async getAll(): Promise<UserWithSharedLibrariesDto[]> {
|
||||
async getAll(): Promise<UserDto[]> {
|
||||
try {
|
||||
return (await this.http.get(`${API_USERS}`)).data
|
||||
} catch (e) {
|
||||
|
|
@ -74,9 +74,9 @@ export default class KomgaUsersService {
|
|||
}
|
||||
}
|
||||
|
||||
async patchUserRoles(userId: string, roles: RolesUpdateDto): Promise<UserDto> {
|
||||
async patchUser(userId: string, patch: UserUpdateDto) {
|
||||
try {
|
||||
return (await this.http.patch(`${API_USERS}/${userId}`, roles)).data
|
||||
await this.http.patch(`${API_USERS}/${userId}`, patch)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to patch user '${userId}'`
|
||||
if (e.response.data.message) {
|
||||
|
|
@ -100,7 +100,7 @@ export default class KomgaUsersService {
|
|||
|
||||
async patchUserPassword(user: UserDto, newPassword: PasswordUpdateDto) {
|
||||
try {
|
||||
return (await this.http.patch(`${API_USERS}/${user.id}/password`, newPassword)).data
|
||||
await this.http.patch(`${API_USERS}/${user.id}/password`, newPassword)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to update password for user ${user.email}`
|
||||
if (e.response.data.message) {
|
||||
|
|
@ -110,21 +110,9 @@ export default class KomgaUsersService {
|
|||
}
|
||||
}
|
||||
|
||||
async patchUserSharedLibraries(user: UserDto, sharedLibrariesUpdateDto: SharedLibrariesUpdateDto) {
|
||||
try {
|
||||
return (await this.http.patch(`${API_USERS}/${user.id}/shared-libraries`, sharedLibrariesUpdateDto)).data
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to update shared libraries for user ${user.email}`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await this.http.post(`${API_USERS}/logout`)
|
||||
await this.http.post('api/logout')
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to logout'
|
||||
if (e.response.data.message) {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,9 @@
|
|||
interface UserDto {
|
||||
id: string,
|
||||
email: string,
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
interface UserWithSharedLibrariesDto {
|
||||
id: string,
|
||||
email: string,
|
||||
roles: string[],
|
||||
sharedAllLibraries: boolean,
|
||||
sharedLibraries: SharedLibraryDto[]
|
||||
}
|
||||
|
||||
interface SharedLibraryDto {
|
||||
id: string
|
||||
sharedLibrariesIds: string[]
|
||||
}
|
||||
|
||||
interface UserCreationDto {
|
||||
|
|
@ -25,13 +15,12 @@ interface PasswordUpdateDto {
|
|||
password: string
|
||||
}
|
||||
|
||||
interface SharedLibrariesUpdateDto {
|
||||
all: boolean,
|
||||
libraryIds: string[]
|
||||
}
|
||||
|
||||
interface RolesUpdateDto {
|
||||
roles: string[]
|
||||
interface UserUpdateDto {
|
||||
roles?: string[],
|
||||
sharedLibraries?: {
|
||||
all: boolean,
|
||||
libraryIds: string[]
|
||||
},
|
||||
}
|
||||
|
||||
interface AuthenticationActivityDto {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class SecurityConfiguration(
|
|||
it.authenticationDetailsSource(userAgentWebAuthenticationDetailsSource)
|
||||
}
|
||||
.logout {
|
||||
it.logoutUrl("/api/v1/users/logout")
|
||||
it.logoutUrl("/api/logout")
|
||||
it.deleteCookies(sessionCookieName)
|
||||
it.invalidateHttpSession(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package org.gotson.komga.interfaces.api.rest
|
|||
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.service.KomgaUserLifecycle
|
||||
import org.gotson.komga.interfaces.api.rest.dto.UserDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.UserDtoV2
|
||||
import org.gotson.komga.interfaces.api.rest.dto.toDtoV2
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.validation.annotation.Validated
|
||||
|
|
@ -30,7 +30,7 @@ class ClaimController(
|
|||
fun claimAdmin(
|
||||
@Email(regexp = ".+@.+\\..+") @RequestHeader("X-Komga-Email") email: String,
|
||||
@NotBlank @RequestHeader("X-Komga-Password") password: String,
|
||||
): UserDto {
|
||||
): UserDtoV2 {
|
||||
if (userDetailsLifecycle.countUsers() > 0)
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "This server has already been claimed")
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ class ClaimController(
|
|||
password = password,
|
||||
roleAdmin = true,
|
||||
),
|
||||
).toDto()
|
||||
).toDtoV2()
|
||||
}
|
||||
|
||||
data class ClaimStatus(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.gotson.komga.interfaces.api.rest
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
|
|
@ -46,9 +48,10 @@ import javax.validation.Valid
|
|||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Deprecated("User api/v2/users instead")
|
||||
@RestController
|
||||
@RequestMapping("api/v1/users", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
class UserController(
|
||||
class UserV1Controller(
|
||||
private val userLifecycle: KomgaUserLifecycle,
|
||||
private val userRepository: KomgaUserRepository,
|
||||
private val libraryRepository: LibraryRepository,
|
||||
|
|
@ -59,10 +62,12 @@ class UserController(
|
|||
private val demo = env.activeProfiles.contains("demo")
|
||||
|
||||
@GetMapping("me")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun getMe(@AuthenticationPrincipal principal: KomgaPrincipal): UserDto =
|
||||
principal.user.toDto()
|
||||
|
||||
@PatchMapping("me/password")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun updateMyPassword(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
|
|
@ -76,12 +81,14 @@ class UserController(
|
|||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun getAll(): List<UserWithSharedLibrariesDto> =
|
||||
userRepository.findAll().map { it.toWithSharedLibrariesDto() }
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun addOne(@Valid @RequestBody newUser: UserCreationDto): UserDto =
|
||||
try {
|
||||
userLifecycle.createUser(newUser.toDomain()).toDto()
|
||||
|
|
@ -92,6 +99,7 @@ class UserController(
|
|||
@DeleteMapping("{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN') and #principal.user.id != #id")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun delete(
|
||||
@PathVariable id: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
|
|
@ -104,6 +112,7 @@ class UserController(
|
|||
@PatchMapping("{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN') and #principal.user.id != #id")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun updateUserRoles(
|
||||
@PathVariable id: String,
|
||||
@Valid @RequestBody patch: RolesUpdateDto,
|
||||
|
|
@ -123,6 +132,7 @@ class UserController(
|
|||
@PatchMapping("{id}/password")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN') or #principal.user.id == #id")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun updatePassword(
|
||||
@PathVariable id: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
|
|
@ -137,6 +147,7 @@ class UserController(
|
|||
@PatchMapping("{id}/shared-libraries")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun updateSharesLibraries(
|
||||
@PathVariable id: String,
|
||||
@Valid @RequestBody sharedLibrariesUpdateDto: SharedLibrariesUpdateDto,
|
||||
|
|
@ -156,6 +167,7 @@ class UserController(
|
|||
|
||||
@GetMapping("me/authentication-activity")
|
||||
@PageableAsQueryParam
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun getMyAuthenticationActivity(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
|
||||
|
|
@ -180,6 +192,7 @@ class UserController(
|
|||
@GetMapping("authentication-activity")
|
||||
@PageableAsQueryParam
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun getAuthenticationActivity(
|
||||
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
|
|
@ -201,6 +214,7 @@ class UserController(
|
|||
|
||||
@GetMapping("{id}/authentication-activity/latest")
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN') or #principal.user.id == #id")
|
||||
@Deprecated("User api/v2/users instead")
|
||||
fun getLatestAuthenticationActivityForUser(
|
||||
@PathVariable id: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
package org.gotson.komga.interfaces.api.rest
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
|
||||
import org.gotson.komga.domain.model.UserEmailAlreadyExistsException
|
||||
import org.gotson.komga.domain.persistence.AuthenticationActivityRepository
|
||||
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.infrastructure.jooq.UnpagedSorted
|
||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
import org.gotson.komga.interfaces.api.rest.dto.AuthenticationActivityDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.PasswordUpdateDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.UserCreationDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.UserDtoV2
|
||||
import org.gotson.komga.interfaces.api.rest.dto.UserUpdateDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.toDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.toDtoV2
|
||||
import org.springdoc.core.converters.models.PageableAsQueryParam
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PatchMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import javax.validation.Valid
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/v2/users", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
class UserV2Controller(
|
||||
private val userLifecycle: KomgaUserLifecycle,
|
||||
private val userRepository: KomgaUserRepository,
|
||||
private val libraryRepository: LibraryRepository,
|
||||
private val authenticationActivityRepository: AuthenticationActivityRepository,
|
||||
env: Environment,
|
||||
) {
|
||||
|
||||
private val demo = env.activeProfiles.contains("demo")
|
||||
|
||||
@GetMapping("me")
|
||||
fun getMe(@AuthenticationPrincipal principal: KomgaPrincipal): UserDtoV2 =
|
||||
principal.toDtoV2()
|
||||
|
||||
@PatchMapping("me/password")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
fun updateMyPassword(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@Valid @RequestBody newPasswordDto: PasswordUpdateDto,
|
||||
) {
|
||||
if (demo) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
userRepository.findByEmailIgnoreCaseOrNull(principal.username)?.let { user ->
|
||||
userLifecycle.updatePassword(user, newPasswordDto.password, false)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
fun getAll(): List<UserDtoV2> =
|
||||
userRepository.findAll().map { it.toDtoV2() }
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
fun addOne(@Valid @RequestBody newUser: UserCreationDto): UserDtoV2 =
|
||||
try {
|
||||
userLifecycle.createUser(newUser.toDomain()).toDtoV2()
|
||||
} catch (e: UserEmailAlreadyExistsException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "A user with this email already exists")
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN') and #principal.user.id != #id")
|
||||
fun delete(
|
||||
@PathVariable id: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
) {
|
||||
userRepository.findByIdOrNull(id)?.let {
|
||||
userLifecycle.deleteUser(it)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
@PatchMapping("{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN') and #principal.user.id != #id")
|
||||
fun updateUser(
|
||||
@PathVariable id: String,
|
||||
@Valid @RequestBody patch: UserUpdateDto,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
) {
|
||||
userRepository.findByIdOrNull(id)?.let { existing ->
|
||||
val updatedUser = with(patch) {
|
||||
existing.copy(
|
||||
roleAdmin = if (isSet("roles")) roles!!.contains(ROLE_ADMIN) else existing.roleAdmin,
|
||||
roleFileDownload = if (isSet("roles")) roles!!.contains(ROLE_FILE_DOWNLOAD) else existing.roleFileDownload,
|
||||
rolePageStreaming = if (isSet("roles")) roles!!.contains(ROLE_PAGE_STREAMING) else existing.rolePageStreaming,
|
||||
sharedAllLibraries = if (isSet("sharedLibraries")) sharedLibraries!!.all else existing.sharedAllLibraries,
|
||||
sharedLibrariesIds = if (isSet("sharedLibraries")) {
|
||||
if (sharedLibraries!!.all) emptySet()
|
||||
else libraryRepository.findAllByIds(sharedLibraries!!.libraryIds).map { it.id }.toSet()
|
||||
} else existing.sharedLibrariesIds,
|
||||
)
|
||||
}
|
||||
userRepository.update(updatedUser)
|
||||
logger.info { "Updated user: $updatedUser" }
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
@PatchMapping("{id}/password")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN') or #principal.user.id == #id")
|
||||
fun updatePassword(
|
||||
@PathVariable id: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@Valid @RequestBody newPasswordDto: PasswordUpdateDto,
|
||||
) {
|
||||
if (demo) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
userRepository.findByIdOrNull(id)?.let { user ->
|
||||
userLifecycle.updatePassword(user, newPasswordDto.password, user.id != principal.user.id)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
@GetMapping("me/authentication-activity")
|
||||
@PageableAsQueryParam
|
||||
fun getMyAuthenticationActivity(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): Page<AuthenticationActivityDto> {
|
||||
if (demo && !principal.user.roleAdmin) throw ResponseStatusException(HttpStatus.FORBIDDEN)
|
||||
val sort =
|
||||
if (page.sort.isSorted) page.sort
|
||||
else Sort.by(Sort.Order.desc("dateTime"))
|
||||
|
||||
val pageRequest =
|
||||
if (unpaged) UnpagedSorted(sort)
|
||||
else PageRequest.of(
|
||||
page.pageNumber,
|
||||
page.pageSize,
|
||||
sort,
|
||||
)
|
||||
|
||||
return authenticationActivityRepository.findAllByUser(principal.user, pageRequest).map { it.toDto() }
|
||||
}
|
||||
|
||||
@GetMapping("authentication-activity")
|
||||
@PageableAsQueryParam
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
fun getAuthenticationActivity(
|
||||
@RequestParam(name = "unpaged", required = false) unpaged: Boolean = false,
|
||||
@Parameter(hidden = true) page: Pageable,
|
||||
): Page<AuthenticationActivityDto> {
|
||||
val sort =
|
||||
if (page.sort.isSorted) page.sort
|
||||
else Sort.by(Sort.Order.desc("dateTime"))
|
||||
|
||||
val pageRequest =
|
||||
if (unpaged) UnpagedSorted(sort)
|
||||
else PageRequest.of(
|
||||
page.pageNumber,
|
||||
page.pageSize,
|
||||
sort,
|
||||
)
|
||||
|
||||
return authenticationActivityRepository.findAll(pageRequest).map { it.toDto() }
|
||||
}
|
||||
|
||||
@GetMapping("{id}/authentication-activity/latest")
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN') or #principal.user.id == #id")
|
||||
fun getLatestAuthenticationActivityForUser(
|
||||
@PathVariable id: String,
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
): AuthenticationActivityDto =
|
||||
userRepository.findByIdOrNull(id)?.let { user ->
|
||||
authenticationActivityRepository.findMostRecentByUser(user)?.toDto()
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
|
@ -8,12 +8,14 @@ import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
|||
import javax.validation.constraints.Email
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
@Deprecated("Deprecated since 0.153.0. Use UserDtoV2 instead")
|
||||
data class UserDto(
|
||||
val id: String,
|
||||
val email: String,
|
||||
val roles: List<String>,
|
||||
)
|
||||
|
||||
@Deprecated("Deprecated since 0.153.0. Use toDtoV2() instead")
|
||||
fun KomgaUser.toDto() =
|
||||
UserDto(
|
||||
id = id,
|
||||
|
|
@ -21,8 +23,29 @@ fun KomgaUser.toDto() =
|
|||
roles = roles().toList(),
|
||||
)
|
||||
|
||||
@Deprecated("Deprecated since 0.153.0. Use toDtoV2() instead")
|
||||
fun KomgaPrincipal.toDto() = user.toDto()
|
||||
|
||||
data class UserDtoV2(
|
||||
val id: String,
|
||||
val email: String,
|
||||
val roles: Set<String>,
|
||||
val sharedAllLibraries: Boolean,
|
||||
val sharedLibrariesIds: Set<String>,
|
||||
)
|
||||
|
||||
fun KomgaUser.toDtoV2() =
|
||||
UserDtoV2(
|
||||
id = id,
|
||||
email = email,
|
||||
roles = roles(),
|
||||
sharedAllLibraries = sharedAllLibraries,
|
||||
sharedLibrariesIds = sharedLibrariesIds,
|
||||
)
|
||||
|
||||
fun KomgaPrincipal.toDtoV2() = user.toDtoV2()
|
||||
|
||||
@Deprecated("Deprecated since 0.153.0. Use UserDtoV2 instead")
|
||||
data class UserWithSharedLibrariesDto(
|
||||
val id: String,
|
||||
val email: String,
|
||||
|
|
@ -31,10 +54,12 @@ data class UserWithSharedLibrariesDto(
|
|||
val sharedLibraries: List<SharedLibraryDto>,
|
||||
)
|
||||
|
||||
@Deprecated("Deprecated since 0.153.0. Use UserDtoV2 instead")
|
||||
data class SharedLibraryDto(
|
||||
val id: String,
|
||||
)
|
||||
|
||||
@Deprecated("Deprecated since 0.153.0. Use toDtoV2() instead")
|
||||
fun KomgaUser.toWithSharedLibrariesDto() =
|
||||
UserWithSharedLibrariesDto(
|
||||
id = id,
|
||||
|
|
@ -63,11 +88,7 @@ data class PasswordUpdateDto(
|
|||
@get:NotBlank val password: String,
|
||||
)
|
||||
|
||||
data class SharedLibrariesUpdateDto(
|
||||
val all: Boolean,
|
||||
val libraryIds: Set<String>,
|
||||
)
|
||||
|
||||
@Deprecated("Deprecated since 0.153.0")
|
||||
data class RolesUpdateDto(
|
||||
val roles: List<String>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package org.gotson.komga.interfaces.api.rest.dto
|
||||
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class UserUpdateDto {
|
||||
private val isSet = mutableMapOf<String, Boolean>()
|
||||
fun isSet(prop: String) = isSet.getOrDefault(prop, false)
|
||||
|
||||
// @get:NullOrNotBlank
|
||||
// val title: String? = null
|
||||
|
||||
// @get:PositiveOrZero
|
||||
// var ageRating: Int?
|
||||
// by Delegates.observable(null) { prop, _, _ ->
|
||||
// isSet[prop.name] = true
|
||||
// }
|
||||
|
||||
var roles: Set<String>?
|
||||
by Delegates.observable(null) { prop, _, _ ->
|
||||
isSet[prop.name] = true
|
||||
}
|
||||
|
||||
var sharedLibraries: SharedLibrariesUpdateDto?
|
||||
by Delegates.observable(null) { prop, _, _ ->
|
||||
isSet[prop.name] = true
|
||||
}
|
||||
}
|
||||
|
||||
data class SharedLibrariesUpdateDto(
|
||||
val all: Boolean,
|
||||
val libraryIds: Set<String>,
|
||||
)
|
||||
|
|
@ -9,6 +9,7 @@ import org.springframework.http.MediaType
|
|||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.get
|
||||
import org.springframework.test.web.servlet.patch
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
|
|
@ -24,11 +25,20 @@ class UserControllerDemoTest(
|
|||
fun `given demo profile is active when a user tries to update its password via api then returns forbidden`() {
|
||||
val jsonString = """{"password":"new"}"""
|
||||
|
||||
mockMvc.patch("/api/v1/users/me/password") {
|
||||
mockMvc.patch("/api/v2/users/me/password") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isForbidden() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser
|
||||
fun `given demo profile is active when a user tries to retrieve own authentication activity then returns forbidden`() {
|
||||
mockMvc.get("/api/v2/users/me/authentication-activity")
|
||||
.andExpect {
|
||||
status { isForbidden() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
package org.gotson.komga.interfaces.api.rest
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ROLE_FILE_DOWNLOAD
|
||||
import org.gotson.komga.domain.model.ROLE_PAGE_STREAMING
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
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.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.ValueSource
|
||||
|
|
@ -11,6 +25,7 @@ import org.springframework.http.MediaType
|
|||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.patch
|
||||
import org.springframework.test.web.servlet.post
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
|
|
@ -19,19 +34,192 @@ import org.springframework.test.web.servlet.post
|
|||
@ActiveProfiles("test")
|
||||
class UserControllerTest(
|
||||
@Autowired private val mockMvc: MockMvc,
|
||||
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val libraryLifecycle: LibraryLifecycle,
|
||||
@Autowired private val userRepository: KomgaUserRepository,
|
||||
@Autowired private val userLifecycle: KomgaUserLifecycle,
|
||||
) {
|
||||
private val admin = KomgaUser("admin@example.org", "", true, id = "admin")
|
||||
|
||||
@BeforeAll
|
||||
fun setup() {
|
||||
libraryRepository.insert(makeLibrary(id = "1"))
|
||||
libraryRepository.insert(makeLibrary(id = "2"))
|
||||
userRepository.insert(admin)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
userRepository.findAll().forEach {
|
||||
userLifecycle.deleteUser(it)
|
||||
}
|
||||
libraryRepository.findAll().forEach {
|
||||
libraryLifecycle.deleteLibrary(it)
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun deleteUsers() {
|
||||
userRepository.findAll()
|
||||
.filterNot { it.email == admin.email }
|
||||
.forEach { userLifecycle.deleteUser(it) }
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = ["user", "user@domain"])
|
||||
@WithMockCustomUser(roles = [ROLE_ADMIN])
|
||||
fun `when creating a user with invalid email then returns bad request`(email: String) {
|
||||
val jsonString = """{"email":"$email","password":"password"}"""
|
||||
|
||||
mockMvc.post("/api/v1/users") {
|
||||
mockMvc.post("/api/v2/users") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isBadRequest() }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Update {
|
||||
@Test
|
||||
@WithMockCustomUser(id = "admin", roles = [ROLE_ADMIN])
|
||||
fun `given user without roles when updating roles then roles are updated`() {
|
||||
val user = KomgaUser("user@example.org", "", false, id = "user", roleFileDownload = false, rolePageStreaming = false)
|
||||
userLifecycle.createUser(user)
|
||||
|
||||
val jsonString = """
|
||||
{
|
||||
"roles": ["$ROLE_FILE_DOWNLOAD","$ROLE_PAGE_STREAMING"]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
mockMvc.patch("/api/v2/users/${user.id}") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isNoContent() }
|
||||
}
|
||||
|
||||
with(userRepository.findByIdOrNull(user.id)) {
|
||||
assertThat(this).isNotNull
|
||||
assertThat(this!!.roleFileDownload).isTrue
|
||||
assertThat(this.rolePageStreaming).isTrue
|
||||
assertThat(this.roleAdmin).isFalse
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(id = "admin", roles = [ROLE_ADMIN])
|
||||
fun `given user with roles when updating roles then roles are updated`() {
|
||||
val user = KomgaUser("user@example.org", "", true, id = "user", roleFileDownload = true, rolePageStreaming = true)
|
||||
userLifecycle.createUser(user)
|
||||
|
||||
val jsonString = """
|
||||
{
|
||||
"roles": []
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
mockMvc.patch("/api/v2/users/${user.id}") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isNoContent() }
|
||||
}
|
||||
|
||||
with(userRepository.findByIdOrNull(user.id)) {
|
||||
assertThat(this).isNotNull
|
||||
assertThat(this!!.roleFileDownload).isFalse
|
||||
assertThat(this.rolePageStreaming).isFalse
|
||||
assertThat(this.roleAdmin).isFalse
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(id = "admin", roles = [ROLE_ADMIN])
|
||||
fun `given user with library restrictions when updating available libraries then they are updated`() {
|
||||
val user = KomgaUser("user@example.org", "", false, id = "user", sharedAllLibraries = false, sharedLibrariesIds = setOf("1"))
|
||||
userLifecycle.createUser(user)
|
||||
|
||||
val jsonString = """
|
||||
{
|
||||
"sharedLibraries": {
|
||||
"all": "false",
|
||||
"libraryIds" : ["1", "2"]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
mockMvc.patch("/api/v2/users/${user.id}") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isNoContent() }
|
||||
}
|
||||
|
||||
with(userRepository.findByIdOrNull(user.id)) {
|
||||
assertThat(this).isNotNull
|
||||
assertThat(this!!.sharedAllLibraries).isFalse
|
||||
assertThat(this.sharedLibrariesIds).containsExactlyInAnyOrder("1", "2")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(id = "admin", roles = [ROLE_ADMIN])
|
||||
fun `given user without library restrictions when restricting libraries then they restrictions are updated`() {
|
||||
val user = KomgaUser("user@example.org", "", false, id = "user", sharedAllLibraries = true)
|
||||
userLifecycle.createUser(user)
|
||||
|
||||
val jsonString = """
|
||||
{
|
||||
"sharedLibraries": {
|
||||
"all": "false",
|
||||
"libraryIds" : ["2"]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
mockMvc.patch("/api/v2/users/${user.id}") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isNoContent() }
|
||||
}
|
||||
|
||||
with(userRepository.findByIdOrNull(user.id)) {
|
||||
assertThat(this).isNotNull
|
||||
assertThat(this!!.sharedAllLibraries).isFalse
|
||||
assertThat(this.sharedLibrariesIds).containsExactlyInAnyOrder("2")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockCustomUser(id = "admin", roles = [ROLE_ADMIN])
|
||||
fun `given user with library restrictions when removing restrictions then they restrictions are updated`() {
|
||||
val user = KomgaUser("user@example.org", "", false, id = "user", sharedAllLibraries = false, sharedLibrariesIds = setOf("2"))
|
||||
userLifecycle.createUser(user)
|
||||
|
||||
val jsonString = """
|
||||
{
|
||||
"sharedLibraries": {
|
||||
"all": "true",
|
||||
"libraryIds": []
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
mockMvc.patch("/api/v2/users/${user.id}") {
|
||||
contentType = MediaType.APPLICATION_JSON
|
||||
content = jsonString
|
||||
}.andExpect {
|
||||
status { isNoContent() }
|
||||
}
|
||||
|
||||
with(userRepository.findByIdOrNull(user.id)) {
|
||||
assertThat(this).isNotNull
|
||||
assertThat(this!!.sharedAllLibraries).isTrue
|
||||
assertThat(this.sharedLibrariesIds).isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue