Start implementation of game requests

This commit is contained in:
grimsi
2025-08-31 15:56:56 +02:00
parent d5b2eb039e
commit 6b81d3904c
25 changed files with 250 additions and 73 deletions
@@ -0,0 +1,10 @@
package org.gameyfin.app.core.plugins.dto
class ExternalProviderIdDto(
val pluginId: String,
val externalProviderId: String,
) {
override fun toString(): String {
return "$pluginId:$externalProviderId"
}
}
@@ -1,9 +1,14 @@
package org.gameyfin.app.core.security package org.gameyfin.app.core.security
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
fun getCurrentAuth(): Authentication {
return SecurityContextHolder.getContext().authentication
}
fun isCurrentUserAdmin(): Boolean { fun isCurrentUserAdmin(): Boolean {
return SecurityContextHolder.getContext().authentication?.authorities?.any { it.authority == Role.Names.ADMIN || it.authority == Role.Names.SUPERADMIN } return getCurrentAuth().authorities?.any { it.authority == Role.Names.ADMIN || it.authority == Role.Names.SUPERADMIN }
?: false ?: false
} }
@@ -5,8 +5,12 @@ import com.vaadin.hilla.Endpoint
import jakarta.annotation.security.RolesAllowed import jakarta.annotation.security.RolesAllowed
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
import org.gameyfin.app.core.annotations.DynamicPublicAccess import org.gameyfin.app.core.annotations.DynamicPublicAccess
import org.gameyfin.app.core.plugins.dto.ExternalProviderIdDto
import org.gameyfin.app.core.security.isCurrentUserAdmin import org.gameyfin.app.core.security.isCurrentUserAdmin
import org.gameyfin.app.games.dto.* import org.gameyfin.app.games.dto.GameDto
import org.gameyfin.app.games.dto.GameEvent
import org.gameyfin.app.games.dto.GameSearchResultDto
import org.gameyfin.app.games.dto.GameUpdateDto
import org.gameyfin.app.libraries.LibraryCoreService import org.gameyfin.app.libraries.LibraryCoreService
import org.gameyfin.app.libraries.LibraryService import org.gameyfin.app.libraries.LibraryService
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -45,7 +49,12 @@ class GameEndpoint(
} }
@RolesAllowed(Role.Names.ADMIN) @RolesAllowed(Role.Names.ADMIN)
fun matchManually(originalIds: Map<String, OriginalIdDto>, path: String, libraryId: Long, replaceGameId: Long?) { fun matchManually(
originalIds: Map<String, ExternalProviderIdDto>,
path: String,
libraryId: Long,
replaceGameId: Long?
) {
val library = libraryService.getById(libraryId) val library = libraryService.getById(libraryId)
val game = gameService.matchManually(originalIds, Path.of(path), library, replaceGameId) val game = gameService.matchManually(originalIds, Path.of(path), library, replaceGameId)
if (game != null) { if (game != null) {
@@ -12,10 +12,12 @@ import org.gameyfin.app.core.alphaNumeric
import org.gameyfin.app.core.filesystem.FilesystemService import org.gameyfin.app.core.filesystem.FilesystemService
import org.gameyfin.app.core.filterValuesNotNull import org.gameyfin.app.core.filterValuesNotNull
import org.gameyfin.app.core.plugins.PluginService import org.gameyfin.app.core.plugins.PluginService
import org.gameyfin.app.core.plugins.dto.ExternalProviderIdDto
import org.gameyfin.app.core.plugins.management.GameyfinPluginDescriptor import org.gameyfin.app.core.plugins.management.GameyfinPluginDescriptor
import org.gameyfin.app.core.plugins.management.GameyfinPluginManager import org.gameyfin.app.core.plugins.management.GameyfinPluginManager
import org.gameyfin.app.core.plugins.management.PluginManagementEntry import org.gameyfin.app.core.plugins.management.PluginManagementEntry
import org.gameyfin.app.core.replaceRomanNumerals import org.gameyfin.app.core.replaceRomanNumerals
import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.games.dto.* import org.gameyfin.app.games.dto.*
import org.gameyfin.app.games.entities.* import org.gameyfin.app.games.entities.*
import org.gameyfin.app.games.extensions.toDtos import org.gameyfin.app.games.extensions.toDtos
@@ -25,7 +27,6 @@ import org.gameyfin.app.media.ImageService
import org.gameyfin.app.users.UserService import org.gameyfin.app.users.UserService
import org.gameyfin.pluginapi.gamemetadata.* import org.gameyfin.pluginapi.gamemetadata.*
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.oauth2.core.oidc.user.OidcUser import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -149,8 +150,7 @@ class GameService(
val existingGame = gameRepository.findByIdOrNull(gameUpdateDto.id) val existingGame = gameRepository.findByIdOrNull(gameUpdateDto.id)
?: throw IllegalArgumentException("Game with ID $gameUpdateDto.id not found") ?: throw IllegalArgumentException("Game with ID $gameUpdateDto.id not found")
val userDetails = SecurityContextHolder.getContext().authentication.principal val user = when (val userDetails = getCurrentAuth().principal) {
val user = when (userDetails) {
is UserDetails -> userService.getByUsernameNonNull(userDetails.username) is UserDetails -> userService.getByUsernameNonNull(userDetails.username)
is OidcUser -> userService.getByUsernameNonNull(userDetails.preferredUsername) is OidcUser -> userService.getByUsernameNonNull(userDetails.preferredUsername)
else -> throw IllegalStateException("Unkown user type: ${userDetails::class.java.name}") else -> throw IllegalStateException("Unkown user type: ${userDetails::class.java.name}")
@@ -260,12 +260,12 @@ class GameService(
val game = getById(game.id!!) val game = getById(game.id!!)
val originalIds: Map<String, OriginalIdDto> = game.metadata.originalIds val originalIds: Map<String, ExternalProviderIdDto> = game.metadata.originalIds
.map { (provider, originalId) -> .map { (provider, originalId) ->
val providerId = pluginManager.getExtensions(provider.pluginId).first()?.javaClass?.name ?: return null val providerId = pluginManager.getExtensions(provider.pluginId).first()?.javaClass?.name ?: return null
val pluginId = provider.pluginId val pluginId = provider.pluginId
val originalId = originalId val originalId = originalId
providerId to OriginalIdDto(pluginId, originalId) providerId to ExternalProviderIdDto(pluginId, originalId)
} }
.toMap() .toMap()
@@ -504,12 +504,12 @@ class GameService(
sorted.mapNotNull { selector(it.second) }.firstOrNull { it.isNotEmpty() } sorted.mapNotNull { selector(it.second) }.firstOrNull { it.isNotEmpty() }
// Collect originalIds for this group // Collect originalIds for this group
val originalIds: Map<String, OriginalIdDto> = group val originalIds: Map<String, ExternalProviderIdDto> = group
.mapNotNull { (provider, metadata) -> .mapNotNull { (provider, metadata) ->
val providerId = provider.javaClass.name val providerId = provider.javaClass.name
val pluginId = providerToManagementEntry[provider]?.pluginId ?: return@mapNotNull null val pluginId = providerToManagementEntry[provider]?.pluginId ?: return@mapNotNull null
val originalId = metadata.originalId val originalId = metadata.originalId
if (providerId != null) providerId to OriginalIdDto(pluginId, originalId) else null if (providerId != null) providerId to ExternalProviderIdDto(pluginId, originalId) else null
} }
.toMap() .toMap()
@@ -567,7 +567,7 @@ class GameService(
} }
fun matchManually( fun matchManually(
originalIds: Map<String, OriginalIdDto>, originalIds: Map<String, ExternalProviderIdDto>,
path: Path, path: Path,
library: Library, library: Library,
replaceGameId: Long? = null, replaceGameId: Long? = null,
@@ -578,7 +578,7 @@ class GameService(
coroutineScope { coroutineScope {
metadataPlugins.associateWith { plugin -> metadataPlugins.associateWith { plugin ->
async { async {
val originalId = originalIds[plugin.javaClass.name]?.originalId ?: return@async null val originalId = originalIds[plugin.javaClass.name]?.externalProviderId ?: return@async null
try { try {
return@async plugin.fetchById(originalId) return@async plugin.fetchById(originalId)
} catch (e: Exception) { } catch (e: Exception) {
@@ -1,5 +1,6 @@
package org.gameyfin.app.games.dto package org.gameyfin.app.games.dto
import org.gameyfin.app.core.plugins.dto.ExternalProviderIdDto
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@@ -11,18 +12,9 @@ class GameSearchResultDto(
val release: Instant?, val release: Instant?,
val publishers: Collection<String>?, val publishers: Collection<String>?,
val developers: Collection<String>?, val developers: Collection<String>?,
val originalIds: Map<String, OriginalIdDto> val originalIds: Map<String, ExternalProviderIdDto>
) )
class OriginalIdDto(
val pluginId: String,
val originalId: String,
) {
override fun toString(): String {
return "$pluginId:$originalId"
}
}
class UrlWithSourceDto( class UrlWithSourceDto(
val url: String, val url: String,
val pluginId: String val pluginId: String
@@ -7,6 +7,7 @@ import org.gameyfin.app.core.Role
import org.gameyfin.app.core.Utils import org.gameyfin.app.core.Utils
import org.gameyfin.app.core.annotations.DynamicPublicAccess import org.gameyfin.app.core.annotations.DynamicPublicAccess
import org.gameyfin.app.core.plugins.PluginService import org.gameyfin.app.core.plugins.PluginService
import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.games.entities.Image import org.gameyfin.app.games.entities.Image
import org.gameyfin.app.games.entities.ImageType import org.gameyfin.app.games.entities.ImageType
import org.gameyfin.app.users.UserService import org.gameyfin.app.users.UserService
@@ -15,8 +16,6 @@ import org.springframework.core.io.InputStreamResource
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
@@ -61,7 +60,7 @@ class ImageEndpoint(
@PermitAll @PermitAll
@PostMapping("/avatar/upload") @PostMapping("/avatar/upload")
fun uploadAvatar(@RequestParam("file") file: MultipartFile) { fun uploadAvatar(@RequestParam("file") file: MultipartFile) {
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth = getCurrentAuth()
val image: Image = if (!userService.hasAvatar(auth.name)) { val image: Image = if (!userService.hasAvatar(auth.name)) {
imageService.createFile(ImageType.AVATAR, file.inputStream, file.contentType!!) imageService.createFile(ImageType.AVATAR, file.inputStream, file.contentType!!)
@@ -76,7 +75,7 @@ class ImageEndpoint(
@PermitAll @PermitAll
@PostMapping("/avatar/delete") @PostMapping("/avatar/delete")
fun deleteAvatar() { fun deleteAvatar() {
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth = getCurrentAuth()
userService.deleteAvatar(auth.name) userService.deleteAvatar(auth.name)
} }
@@ -3,6 +3,7 @@ package org.gameyfin.app.messages
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.gameyfin.app.core.events.* import org.gameyfin.app.core.events.*
import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.messages.providers.AbstractMessageProvider import org.gameyfin.app.messages.providers.AbstractMessageProvider
import org.gameyfin.app.messages.templates.MessageTemplateService import org.gameyfin.app.messages.templates.MessageTemplateService
import org.gameyfin.app.messages.templates.MessageTemplates import org.gameyfin.app.messages.templates.MessageTemplates
@@ -10,8 +11,6 @@ import org.gameyfin.app.users.UserService
import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContext
import org.springframework.context.event.EventListener import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async import org.springframework.scheduling.annotation.Async
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.util.* import java.util.*
@@ -61,7 +60,7 @@ class MessageService(
} }
try { try {
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth = getCurrentAuth()
val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found") val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
val template = templateService.getMessageTemplate(templateKey) val template = templateService.getMessageTemplate(templateKey)
sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders) sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders)
@@ -0,0 +1,42 @@
package org.gameyfin.app.requests
import jakarta.persistence.*
import org.gameyfin.app.requests.status.GameRequestStatus
import org.gameyfin.app.users.entities.User
import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.UpdateTimestamp
import java.time.Instant
typealias ExternalProviderIds = Map<String, String>
@Entity
class GameRequest(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null,
@CreationTimestamp
@Column(nullable = false, updatable = false)
var createdAt: Instant? = null,
@UpdateTimestamp
@Column(nullable = false)
var updatedAt: Instant? = null,
@Column(nullable = false)
val title: String,
@Column(nullable = false)
val release: Instant,
@Column(nullable = false)
var status: GameRequestStatus,
@ManyToOne(fetch = FetchType.EAGER)
var requester: User? = null,
var voters: MutableList<User> = mutableListOf(),
@ElementCollection
val externalProviderIds: ExternalProviderIds
)
@@ -0,0 +1,5 @@
package org.gameyfin.app.requests
import org.springframework.data.jpa.repository.JpaRepository
interface GameRequestRepository : JpaRepository<GameRequest, Long>
@@ -0,0 +1,57 @@
package org.gameyfin.app.requests
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.games.dto.GameUserEvent
import org.gameyfin.app.requests.dto.GameRequestCreationDto
import org.gameyfin.app.requests.status.GameRequestStatus
import org.gameyfin.app.users.UserService
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Sinks
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.toJavaDuration
@Service
class GameRequestService(
private val gameRequestRepository: GameRequestRepository,
private val userService: UserService
) {
companion object {
private val log = KotlinLogging.logger {}
/* Websockets */
private val gameRequestEvents = Sinks.many().multicast().onBackpressureBuffer<GameUserEvent>(1024, false)
fun subscribe(): Flux<List<GameUserEvent>> {
log.debug { "New user subscription for gameRequestEvents" }
return gameRequestEvents.asFlux()
.buffer(100.milliseconds.toJavaDuration())
.doOnSubscribe {
log.debug { "Subscriber added to gameRequestEvents [${gameRequestEvents.currentSubscriberCount()}]" }
}
.doFinally {
log.debug { "Subscriber removed from gameRequestEvents with signal type $it [${gameRequestEvents.currentSubscriberCount()}]" }
}
}
fun emit(event: GameUserEvent) {
gameRequestEvents.tryEmitNext(event)
}
}
fun createRequest(gameRequest: GameRequestCreationDto) {
val currentUser = userService.getByUsername(getCurrentAuth().name)
val gameRequest = GameRequest(
title = gameRequest.title,
release = gameRequest.release,
status = GameRequestStatus.PENDING,
externalProviderIds = gameRequest.externalProviderIds,
requester = currentUser
)
gameRequestRepository.save(gameRequest)
}
}
@@ -0,0 +1,10 @@
package org.gameyfin.app.requests.dto
import java.time.Instant
class GameRequestCreationDto(
val title: String,
val release: Instant,
val externalProviderIds: Map<String, String>
)
@@ -0,0 +1,13 @@
package org.gameyfin.app.requests.dto
import org.gameyfin.app.requests.status.GameRequestStatus
import org.gameyfin.app.users.dto.UserInfoAdminDto
import java.time.Instant
class GameRequestDto(
val id: Long,
val title: String,
val release: Instant,
val status: GameRequestStatus,
val requester: UserInfoAdminDto
)
@@ -0,0 +1,9 @@
package org.gameyfin.app.requests.dto
sealed class GameRequestEvent {
abstract val type: String
data class Created(val game: GameRequestDto, override val type: String = "created") : GameRequestEvent()
data class Updated(val game: GameRequestDto, override val type: String = "updated") : GameRequestEvent()
data class Deleted(val gameRequestId: Long, override val type: String = "deleted") : GameRequestEvent()
}
@@ -0,0 +1,15 @@
package org.gameyfin.app.requests.extensions
import org.gameyfin.app.requests.GameRequest
import org.gameyfin.app.requests.dto.GameRequestDto
fun GameRequest.toDto(): GameRequestDto {
return GameRequestDto(
id = this.id!!,
title = this.title,
release = this.release,
externalProviderIds = this.externalProviderIds,
status = this.status,
requester = this.requester.toDto()
)
}
@@ -0,0 +1,8 @@
package org.gameyfin.app.requests.status
enum class GameRequestStatus {
PENDING,
APPROVED,
FULFILLED,
REJECTED
}
@@ -3,7 +3,7 @@ package org.gameyfin.app.setup
import com.vaadin.flow.server.auth.AnonymousAllowed import com.vaadin.flow.server.auth.AnonymousAllowed
import com.vaadin.hilla.Endpoint import com.vaadin.hilla.Endpoint
import com.vaadin.hilla.exception.EndpointException import com.vaadin.hilla.exception.EndpointException
import org.gameyfin.app.users.dto.UserInfoDto import org.gameyfin.app.users.dto.UserInfoAdminDto
import org.gameyfin.app.users.dto.UserRegistrationDto import org.gameyfin.app.users.dto.UserRegistrationDto
@Endpoint @Endpoint
@@ -16,7 +16,7 @@ class SetupEndpoint(
} }
@AnonymousAllowed @AnonymousAllowed
fun registerSuperAdmin(superAdminRegistration: UserRegistrationDto): UserInfoDto { fun registerSuperAdmin(superAdminRegistration: UserRegistrationDto): UserInfoAdminDto {
if (setupService.isSetupCompleted()) throw EndpointException("Setup already completed") if (setupService.isSetupCompleted()) throw EndpointException("Setup already completed")
return setupService.createInitialAdminUser(superAdminRegistration) return setupService.createInitialAdminUser(superAdminRegistration)
} }
@@ -1,9 +1,9 @@
package org.gameyfin.app.setup package org.gameyfin.app.setup
import org.gameyfin.app.users.UserService
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
import org.gameyfin.app.users.RoleService import org.gameyfin.app.users.RoleService
import org.gameyfin.app.users.dto.UserInfoDto import org.gameyfin.app.users.UserService
import org.gameyfin.app.users.dto.UserInfoAdminDto
import org.gameyfin.app.users.dto.UserRegistrationDto import org.gameyfin.app.users.dto.UserRegistrationDto
import org.gameyfin.app.users.entities.User import org.gameyfin.app.users.entities.User
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -26,7 +26,7 @@ class SetupService(
/** /**
* Creates the initial user with Super-Admin permissions * Creates the initial user with Super-Admin permissions
*/ */
fun createInitialAdminUser(registration: UserRegistrationDto): UserInfoDto { fun createInitialAdminUser(registration: UserRegistrationDto): UserInfoAdminDto {
val superAdmin = User( val superAdmin = User(
username = registration.username, username = registration.username,
password = registration.password, password = registration.password,
@@ -1,7 +1,7 @@
package org.gameyfin.app.users package org.gameyfin.app.users
import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.users.entities.User import org.gameyfin.app.users.entities.User
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.session.SessionInformation import org.springframework.security.core.session.SessionInformation
import org.springframework.security.core.session.SessionRegistry import org.springframework.security.core.session.SessionRegistry
@@ -11,15 +11,13 @@ import org.springframework.stereotype.Service
class SessionService(private val sessionRegistry: SessionRegistry) { class SessionService(private val sessionRegistry: SessionRegistry) {
fun logoutAllSessions() { fun logoutAllSessions() {
val auth: Authentication? = SecurityContextHolder.getContext().authentication val auth = getCurrentAuth()
if (auth != null) {
val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(auth.principal, false) val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(auth.principal, false)
for (sessionInfo in sessions) { for (sessionInfo in sessions) {
sessionInfo.expireNow() sessionInfo.expireNow()
} }
SecurityContextHolder.clearContext() SecurityContextHolder.clearContext()
} }
}
fun logoutAllSessions(user: User) { fun logoutAllSessions(user: User) {
val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(user, false) val sessions: List<SessionInformation> = sessionRegistry.getAllSessions(user, false)
@@ -5,11 +5,11 @@ import com.vaadin.hilla.Endpoint
import jakarta.annotation.security.PermitAll import jakarta.annotation.security.PermitAll
import jakarta.annotation.security.RolesAllowed import jakarta.annotation.security.RolesAllowed
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
import org.gameyfin.app.users.dto.UserInfoDto import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.users.dto.UserInfoAdminDto
import org.gameyfin.app.users.dto.UserUpdateDto import org.gameyfin.app.users.dto.UserUpdateDto
import org.gameyfin.app.users.enums.RoleAssignmentResult import org.gameyfin.app.users.enums.RoleAssignmentResult
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
@Endpoint @Endpoint
@@ -18,15 +18,15 @@ class UserEndpoint(
private val roleService: RoleService private val roleService: RoleService
) { ) {
@AnonymousAllowed @AnonymousAllowed
fun getUserInfo(): UserInfoDto? { fun getUserInfo(): UserInfoAdminDto? {
val auth = SecurityContextHolder.getContext().authentication val auth = getCurrentAuth()
if (!auth.isAuthenticated || auth.principal == "anonymousUser") return null if (!auth.isAuthenticated || auth.principal == "anonymousUser") return null
return userService.getUserInfo() return userService.getUserInfo()
} }
@PermitAll @PermitAll
fun updateUser(updates: UserUpdateDto) { fun updateUser(updates: UserUpdateDto) {
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth: Authentication = getCurrentAuth()
userService.updateUser(auth.name, updates) userService.updateUser(auth.name, updates)
} }
@@ -36,7 +36,7 @@ class UserEndpoint(
} }
@RolesAllowed(Role.Names.ADMIN) @RolesAllowed(Role.Names.ADMIN)
fun getAllUsers(): List<UserInfoDto> { fun getAllUsers(): List<UserInfoAdminDto> {
return userService.getAllUsers() return userService.getAllUsers()
} }
@@ -52,7 +52,7 @@ class UserEndpoint(
@PermitAll @PermitAll
fun deleteUser() { fun deleteUser() {
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth: Authentication = getCurrentAuth()
userService.deleteUser(auth.name) userService.deleteUser(auth.name)
} }
@@ -68,7 +68,7 @@ class UserEndpoint(
@RolesAllowed(Role.Names.ADMIN) @RolesAllowed(Role.Names.ADMIN)
fun getRolesBelow(): List<String> { fun getRolesBelow(): List<String> {
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth: Authentication = getCurrentAuth()
return roleService.getRolesBelowAuth(auth).map { it.roleName } return roleService.getRolesBelowAuth(auth).map { it.roleName }
} }
@@ -6,9 +6,10 @@ import org.gameyfin.app.config.ConfigService
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
import org.gameyfin.app.core.Utils import org.gameyfin.app.core.Utils
import org.gameyfin.app.core.events.* import org.gameyfin.app.core.events.*
import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.games.entities.Image import org.gameyfin.app.games.entities.Image
import org.gameyfin.app.media.ImageService import org.gameyfin.app.media.ImageService
import org.gameyfin.app.users.dto.UserInfoDto import org.gameyfin.app.users.dto.UserInfoAdminDto
import org.gameyfin.app.users.dto.UserRegistrationDto import org.gameyfin.app.users.dto.UserRegistrationDto
import org.gameyfin.app.users.dto.UserUpdateDto import org.gameyfin.app.users.dto.UserUpdateDto
import org.gameyfin.app.users.emailconfirmation.EmailConfirmationService import org.gameyfin.app.users.emailconfirmation.EmailConfirmationService
@@ -17,7 +18,6 @@ import org.gameyfin.app.users.persistence.UserRepository
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UserDetailsService
@@ -77,7 +77,7 @@ class UserService(
fun findByOidcProviderId(oidcProviderId: String): org.gameyfin.app.users.entities.User? = fun findByOidcProviderId(oidcProviderId: String): org.gameyfin.app.users.entities.User? =
userRepository.findByOidcProviderId(oidcProviderId) userRepository.findByOidcProviderId(oidcProviderId)
fun getAllUsers(): List<UserInfoDto> { fun getAllUsers(): List<UserInfoAdminDto> {
return userRepository.findAll().map { u -> toUserInfo(u) } return userRepository.findAll().map { u -> toUserInfo(u) }
} }
@@ -93,8 +93,8 @@ class UserService(
return userRepository.findByUsername(username) ?: throw UsernameNotFoundException("Unknown user '$username'") return userRepository.findByUsername(username) ?: throw UsernameNotFoundException("Unknown user '$username'")
} }
fun getUserInfo(): UserInfoDto { fun getUserInfo(): UserInfoAdminDto {
val auth = SecurityContextHolder.getContext().authentication val auth = getCurrentAuth()
val principal = auth.principal val principal = auth.principal
if (principal is OidcUser) { if (principal is OidcUser) {
@@ -238,7 +238,7 @@ class UserService(
return RoleAssignmentResult.NO_ROLES_PROVIDED return RoleAssignmentResult.NO_ROLES_PROVIDED
} }
val currentUser = SecurityContextHolder.getContext().authentication val currentUser = getCurrentAuth()
val targetUser = getByUsernameNonNull(username) val targetUser = getByUsernameNonNull(username)
if (!canManage(targetUser)) { if (!canManage(targetUser)) {
@@ -266,7 +266,7 @@ class UserService(
} }
fun canManage(targetUser: org.gameyfin.app.users.entities.User): Boolean { fun canManage(targetUser: org.gameyfin.app.users.entities.User): Boolean {
val currentUser = SecurityContextHolder.getContext().authentication val currentUser = getCurrentAuth()
val currentUserLevel = roleService.getHighestRoleFromAuthorities(currentUser.authorities).powerLevel val currentUserLevel = roleService.getHighestRoleFromAuthorities(currentUser.authorities).powerLevel
val targetUserLevel = roleService.getHighestRole(targetUser.roles).powerLevel val targetUserLevel = roleService.getHighestRole(targetUser.roles).powerLevel
return currentUserLevel > targetUserLevel return currentUserLevel > targetUserLevel
@@ -285,8 +285,8 @@ class UserService(
eventPublisher.publishEvent(AccountDeletedEvent(this, user, Utils.Companion.getBaseUrl())) eventPublisher.publishEvent(AccountDeletedEvent(this, user, Utils.Companion.getBaseUrl()))
} }
fun toUserInfo(user: org.gameyfin.app.users.entities.User): UserInfoDto { fun toUserInfo(user: org.gameyfin.app.users.entities.User): UserInfoAdminDto {
return UserInfoDto( return UserInfoAdminDto(
username = user.username, username = user.username,
email = user.email, email = user.email,
emailConfirmed = user.emailConfirmed, emailConfirmed = user.emailConfirmed,
@@ -2,7 +2,7 @@ package org.gameyfin.app.users.dto
import org.gameyfin.app.core.Role import org.gameyfin.app.core.Role
data class UserInfoDto( data class UserInfoAdminDto(
val username: String, val username: String,
val managedBySso: Boolean, val managedBySso: Boolean,
val email: String, val email: String,
@@ -0,0 +1,7 @@
package org.gameyfin.app.users.dto
data class UserInfoUserDto(
val username: String,
val hasAvatar: Boolean,
val avatarId: Long? = null,
)
@@ -1,11 +1,10 @@
package org.gameyfin.app.users.emailconfirmation package org.gameyfin.app.users.emailconfirmation
import com.vaadin.hilla.Endpoint import com.vaadin.hilla.Endpoint
import org.gameyfin.app.users.UserService
import jakarta.annotation.security.PermitAll import jakarta.annotation.security.PermitAll
import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.shared.token.TokenValidationResult import org.gameyfin.app.shared.token.TokenValidationResult
import org.springframework.security.core.Authentication import org.gameyfin.app.users.UserService
import org.springframework.security.core.context.SecurityContextHolder
@Endpoint @Endpoint
class EmailConfirmationEndpoint( class EmailConfirmationEndpoint(
@@ -20,7 +19,7 @@ class EmailConfirmationEndpoint(
@PermitAll @PermitAll
fun resendEmailConfirmation() { fun resendEmailConfirmation() {
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth = getCurrentAuth()
userService.getByUsername(auth.name)?.let { userService.getByUsername(auth.name)?.let {
emailConfirmationService.resendEmailConfirmation(it) emailConfirmationService.resendEmailConfirmation(it)
} }
@@ -1,8 +1,8 @@
package org.gameyfin.app.users.preferences package org.gameyfin.app.users.preferences
import org.gameyfin.app.users.UserService
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.security.core.context.SecurityContextHolder import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.users.UserService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.Serializable import java.io.Serializable
@@ -135,7 +135,7 @@ class UserPreferencesService(
} }
private fun id(key: String): UserPreferenceKey { private fun id(key: String): UserPreferenceKey {
val auth = SecurityContextHolder.getContext().authentication val auth = getCurrentAuth()
val user = userService.getByUsernameNonNull(auth.name) val user = userService.getByUsernameNonNull(auth.name)
return UserPreferenceKey(key, user.id!!) return UserPreferenceKey(key, user.id!!)
} }
@@ -1,18 +1,17 @@
package org.gameyfin.app.users.registration package org.gameyfin.app.users.registration
import org.gameyfin.app.core.Utils
import org.gameyfin.app.core.events.AccountStatusChangedEvent import org.gameyfin.app.core.events.AccountStatusChangedEvent
import org.gameyfin.app.core.events.UserInvitationEvent import org.gameyfin.app.core.events.UserInvitationEvent
import org.gameyfin.app.core.security.getCurrentAuth
import org.gameyfin.app.shared.token.TokenDto import org.gameyfin.app.shared.token.TokenDto
import org.gameyfin.app.shared.token.TokenRepository import org.gameyfin.app.shared.token.TokenRepository
import org.gameyfin.app.users.UserService
import org.gameyfin.app.core.Utils
import org.gameyfin.app.shared.token.TokenService import org.gameyfin.app.shared.token.TokenService
import org.gameyfin.app.shared.token.TokenType import org.gameyfin.app.shared.token.TokenType
import org.gameyfin.app.users.UserService
import org.gameyfin.app.users.dto.UserRegistrationDto import org.gameyfin.app.users.dto.UserRegistrationDto
import org.gameyfin.app.users.enums.UserInvitationAcceptanceResult import org.gameyfin.app.users.enums.UserInvitationAcceptanceResult
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
@@ -30,7 +29,7 @@ class InvitationService(
if (userService.existsByEmail(email)) if (userService.existsByEmail(email))
throw IllegalStateException("User with email ${Utils.Companion.maskEmail(email)} is already registered") throw IllegalStateException("User with email ${Utils.Companion.maskEmail(email)} is already registered")
val auth: Authentication = SecurityContextHolder.getContext().authentication val auth = getCurrentAuth()
val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found") val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found")
val payload = mapOf(EMAIL_KEY to email) val payload = mapOf(EMAIL_KEY to email)
val token = super.generateWithPayload(user, payload) val token = super.generateWithPayload(user, payload)
@@ -45,7 +44,8 @@ class InvitationService(
} }
fun acceptInvitation(secret: String, registration: UserRegistrationDto): UserInvitationAcceptanceResult { fun acceptInvitation(secret: String, registration: UserRegistrationDto): UserInvitationAcceptanceResult {
val invitationToken = super.get(secret, TokenType.Invitation) ?: return UserInvitationAcceptanceResult.TOKEN_INVALID val invitationToken =
super.get(secret, TokenType.Invitation) ?: return UserInvitationAcceptanceResult.TOKEN_INVALID
val email = invitationToken.payload[EMAIL_KEY] ?: return UserInvitationAcceptanceResult.TOKEN_INVALID val email = invitationToken.payload[EMAIL_KEY] ?: return UserInvitationAcceptanceResult.TOKEN_INVALID
if (invitationToken.expired) return UserInvitationAcceptanceResult.TOKEN_EXPIRED if (invitationToken.expired) return UserInvitationAcceptanceResult.TOKEN_EXPIRED