diff --git a/app/src/main/kotlin/org/gameyfin/app/core/plugins/dto/ExternalProviderIdDto.kt b/app/src/main/kotlin/org/gameyfin/app/core/plugins/dto/ExternalProviderIdDto.kt new file mode 100644 index 0000000..5881d5c --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/core/plugins/dto/ExternalProviderIdDto.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityUtils.kt b/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityUtils.kt index 1b0adf1..45fc1de 100644 --- a/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityUtils.kt +++ b/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityUtils.kt @@ -1,9 +1,14 @@ package org.gameyfin.app.core.security import org.gameyfin.app.core.Role +import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContextHolder +fun getCurrentAuth(): Authentication { + return SecurityContextHolder.getContext().authentication +} + 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 } \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/games/GameEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/games/GameEndpoint.kt index 5ecd8dd..9f33c51 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/GameEndpoint.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/GameEndpoint.kt @@ -5,8 +5,12 @@ import com.vaadin.hilla.Endpoint import jakarta.annotation.security.RolesAllowed import org.gameyfin.app.core.Role 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.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.LibraryService import reactor.core.publisher.Flux @@ -45,7 +49,12 @@ class GameEndpoint( } @RolesAllowed(Role.Names.ADMIN) - fun matchManually(originalIds: Map, path: String, libraryId: Long, replaceGameId: Long?) { + fun matchManually( + originalIds: Map, + path: String, + libraryId: Long, + replaceGameId: Long? + ) { val library = libraryService.getById(libraryId) val game = gameService.matchManually(originalIds, Path.of(path), library, replaceGameId) if (game != null) { diff --git a/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt b/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt index 751082e..5d42048 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt @@ -12,10 +12,12 @@ import org.gameyfin.app.core.alphaNumeric import org.gameyfin.app.core.filesystem.FilesystemService import org.gameyfin.app.core.filterValuesNotNull 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.GameyfinPluginManager import org.gameyfin.app.core.plugins.management.PluginManagementEntry import org.gameyfin.app.core.replaceRomanNumerals +import org.gameyfin.app.core.security.getCurrentAuth import org.gameyfin.app.games.dto.* import org.gameyfin.app.games.entities.* 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.pluginapi.gamemetadata.* import org.springframework.data.repository.findByIdOrNull -import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.oauth2.core.oidc.user.OidcUser import org.springframework.stereotype.Service @@ -149,8 +150,7 @@ class GameService( val existingGame = gameRepository.findByIdOrNull(gameUpdateDto.id) ?: throw IllegalArgumentException("Game with ID $gameUpdateDto.id not found") - val userDetails = SecurityContextHolder.getContext().authentication.principal - val user = when (userDetails) { + val user = when (val userDetails = getCurrentAuth().principal) { is UserDetails -> userService.getByUsernameNonNull(userDetails.username) is OidcUser -> userService.getByUsernameNonNull(userDetails.preferredUsername) else -> throw IllegalStateException("Unkown user type: ${userDetails::class.java.name}") @@ -260,12 +260,12 @@ class GameService( val game = getById(game.id!!) - val originalIds: Map = game.metadata.originalIds + val originalIds: Map = game.metadata.originalIds .map { (provider, originalId) -> val providerId = pluginManager.getExtensions(provider.pluginId).first()?.javaClass?.name ?: return null val pluginId = provider.pluginId val originalId = originalId - providerId to OriginalIdDto(pluginId, originalId) + providerId to ExternalProviderIdDto(pluginId, originalId) } .toMap() @@ -504,12 +504,12 @@ class GameService( sorted.mapNotNull { selector(it.second) }.firstOrNull { it.isNotEmpty() } // Collect originalIds for this group - val originalIds: Map = group + val originalIds: Map = group .mapNotNull { (provider, metadata) -> val providerId = provider.javaClass.name val pluginId = providerToManagementEntry[provider]?.pluginId ?: return@mapNotNull null 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() @@ -567,7 +567,7 @@ class GameService( } fun matchManually( - originalIds: Map, + originalIds: Map, path: Path, library: Library, replaceGameId: Long? = null, @@ -578,7 +578,7 @@ class GameService( coroutineScope { metadataPlugins.associateWith { plugin -> async { - val originalId = originalIds[plugin.javaClass.name]?.originalId ?: return@async null + val originalId = originalIds[plugin.javaClass.name]?.externalProviderId ?: return@async null try { return@async plugin.fetchById(originalId) } catch (e: Exception) { diff --git a/app/src/main/kotlin/org/gameyfin/app/games/dto/GameSearchResultDto.kt b/app/src/main/kotlin/org/gameyfin/app/games/dto/GameSearchResultDto.kt index 45f98c5..4af2da0 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/dto/GameSearchResultDto.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/dto/GameSearchResultDto.kt @@ -1,5 +1,6 @@ package org.gameyfin.app.games.dto +import org.gameyfin.app.core.plugins.dto.ExternalProviderIdDto import java.time.Instant import java.util.* @@ -11,18 +12,9 @@ class GameSearchResultDto( val release: Instant?, val publishers: Collection?, val developers: Collection?, - val originalIds: Map + val originalIds: Map ) -class OriginalIdDto( - val pluginId: String, - val originalId: String, -) { - override fun toString(): String { - return "$pluginId:$originalId" - } -} - class UrlWithSourceDto( val url: String, val pluginId: String diff --git a/app/src/main/kotlin/org/gameyfin/app/media/ImageEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/media/ImageEndpoint.kt index 0d8b74b..725df8a 100644 --- a/app/src/main/kotlin/org/gameyfin/app/media/ImageEndpoint.kt +++ b/app/src/main/kotlin/org/gameyfin/app/media/ImageEndpoint.kt @@ -7,6 +7,7 @@ import org.gameyfin.app.core.Role import org.gameyfin.app.core.Utils import org.gameyfin.app.core.annotations.DynamicPublicAccess 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.ImageType 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.MediaType 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.multipart.MultipartFile @@ -61,7 +60,7 @@ class ImageEndpoint( @PermitAll @PostMapping("/avatar/upload") fun uploadAvatar(@RequestParam("file") file: MultipartFile) { - val auth: Authentication = SecurityContextHolder.getContext().authentication + val auth = getCurrentAuth() val image: Image = if (!userService.hasAvatar(auth.name)) { imageService.createFile(ImageType.AVATAR, file.inputStream, file.contentType!!) @@ -76,7 +75,7 @@ class ImageEndpoint( @PermitAll @PostMapping("/avatar/delete") fun deleteAvatar() { - val auth: Authentication = SecurityContextHolder.getContext().authentication + val auth = getCurrentAuth() userService.deleteAvatar(auth.name) } diff --git a/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt b/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt index 68a1605..a0e48b8 100644 --- a/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt @@ -3,6 +3,7 @@ package org.gameyfin.app.messages import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging 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.templates.MessageTemplateService 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.event.EventListener 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 java.util.* @@ -61,7 +60,7 @@ class MessageService( } try { - val auth: Authentication = SecurityContextHolder.getContext().authentication + val auth = getCurrentAuth() val user = userService.getByUsername(auth.name) ?: throw IllegalStateException("User not found") val template = templateService.getMessageTemplate(templateKey) sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders) diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/GameRequest.kt b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequest.kt new file mode 100644 index 0000000..6808044 --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequest.kt @@ -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 + +@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 = mutableListOf(), + + @ElementCollection + val externalProviderIds: ExternalProviderIds +) \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestRepository.kt b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestRepository.kt new file mode 100644 index 0000000..20ea18b --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestRepository.kt @@ -0,0 +1,5 @@ +package org.gameyfin.app.requests + +import org.springframework.data.jpa.repository.JpaRepository + +interface GameRequestRepository : JpaRepository \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestService.kt b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestService.kt new file mode 100644 index 0000000..0ea279b --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/requests/GameRequestService.kt @@ -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(1024, false) + + fun subscribe(): Flux> { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestCreationDto.kt b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestCreationDto.kt new file mode 100644 index 0000000..a31f8ed --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestCreationDto.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestDto.kt b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestDto.kt new file mode 100644 index 0000000..1a4169d --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestDto.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestEvent.kt b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestEvent.kt new file mode 100644 index 0000000..f006d3a --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/requests/dto/GameRequestEvent.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/extensions/GameRequestExtensions.kt b/app/src/main/kotlin/org/gameyfin/app/requests/extensions/GameRequestExtensions.kt new file mode 100644 index 0000000..8136b13 --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/requests/extensions/GameRequestExtensions.kt @@ -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() + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/requests/status/GameRequestStatus.kt b/app/src/main/kotlin/org/gameyfin/app/requests/status/GameRequestStatus.kt new file mode 100644 index 0000000..290c677 --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/requests/status/GameRequestStatus.kt @@ -0,0 +1,8 @@ +package org.gameyfin.app.requests.status + +enum class GameRequestStatus { + PENDING, + APPROVED, + FULFILLED, + REJECTED +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/setup/SetupEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/setup/SetupEndpoint.kt index f9be5a1..e020434 100644 --- a/app/src/main/kotlin/org/gameyfin/app/setup/SetupEndpoint.kt +++ b/app/src/main/kotlin/org/gameyfin/app/setup/SetupEndpoint.kt @@ -3,7 +3,7 @@ package org.gameyfin.app.setup import com.vaadin.flow.server.auth.AnonymousAllowed import com.vaadin.hilla.Endpoint 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 @Endpoint @@ -16,7 +16,7 @@ class SetupEndpoint( } @AnonymousAllowed - fun registerSuperAdmin(superAdminRegistration: UserRegistrationDto): UserInfoDto { + fun registerSuperAdmin(superAdminRegistration: UserRegistrationDto): UserInfoAdminDto { if (setupService.isSetupCompleted()) throw EndpointException("Setup already completed") return setupService.createInitialAdminUser(superAdminRegistration) } diff --git a/app/src/main/kotlin/org/gameyfin/app/setup/SetupService.kt b/app/src/main/kotlin/org/gameyfin/app/setup/SetupService.kt index f54fd77..1a6c441 100644 --- a/app/src/main/kotlin/org/gameyfin/app/setup/SetupService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/setup/SetupService.kt @@ -1,9 +1,9 @@ package org.gameyfin.app.setup -import org.gameyfin.app.users.UserService import org.gameyfin.app.core.Role 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.entities.User import org.springframework.stereotype.Service @@ -26,7 +26,7 @@ class SetupService( /** * Creates the initial user with Super-Admin permissions */ - fun createInitialAdminUser(registration: UserRegistrationDto): UserInfoDto { + fun createInitialAdminUser(registration: UserRegistrationDto): UserInfoAdminDto { val superAdmin = User( username = registration.username, password = registration.password, diff --git a/app/src/main/kotlin/org/gameyfin/app/users/SessionService.kt b/app/src/main/kotlin/org/gameyfin/app/users/SessionService.kt index ad5ceb1..3aa2d6c 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/SessionService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/SessionService.kt @@ -1,7 +1,7 @@ package org.gameyfin.app.users +import org.gameyfin.app.core.security.getCurrentAuth 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.session.SessionInformation import org.springframework.security.core.session.SessionRegistry @@ -11,14 +11,12 @@ import org.springframework.stereotype.Service class SessionService(private val sessionRegistry: SessionRegistry) { fun logoutAllSessions() { - val auth: Authentication? = SecurityContextHolder.getContext().authentication - if (auth != null) { - val sessions: List = sessionRegistry.getAllSessions(auth.principal, false) - for (sessionInfo in sessions) { - sessionInfo.expireNow() - } - SecurityContextHolder.clearContext() + val auth = getCurrentAuth() + val sessions: List = sessionRegistry.getAllSessions(auth.principal, false) + for (sessionInfo in sessions) { + sessionInfo.expireNow() } + SecurityContextHolder.clearContext() } fun logoutAllSessions(user: User) { diff --git a/app/src/main/kotlin/org/gameyfin/app/users/UserEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/users/UserEndpoint.kt index 1fc3774..a114da8 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/UserEndpoint.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/UserEndpoint.kt @@ -5,11 +5,11 @@ import com.vaadin.hilla.Endpoint import jakarta.annotation.security.PermitAll import jakarta.annotation.security.RolesAllowed 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.enums.RoleAssignmentResult import org.springframework.security.core.Authentication -import org.springframework.security.core.context.SecurityContextHolder @Endpoint @@ -18,15 +18,15 @@ class UserEndpoint( private val roleService: RoleService ) { @AnonymousAllowed - fun getUserInfo(): UserInfoDto? { - val auth = SecurityContextHolder.getContext().authentication + fun getUserInfo(): UserInfoAdminDto? { + val auth = getCurrentAuth() if (!auth.isAuthenticated || auth.principal == "anonymousUser") return null return userService.getUserInfo() } @PermitAll fun updateUser(updates: UserUpdateDto) { - val auth: Authentication = SecurityContextHolder.getContext().authentication + val auth: Authentication = getCurrentAuth() userService.updateUser(auth.name, updates) } @@ -36,7 +36,7 @@ class UserEndpoint( } @RolesAllowed(Role.Names.ADMIN) - fun getAllUsers(): List { + fun getAllUsers(): List { return userService.getAllUsers() } @@ -52,7 +52,7 @@ class UserEndpoint( @PermitAll fun deleteUser() { - val auth: Authentication = SecurityContextHolder.getContext().authentication + val auth: Authentication = getCurrentAuth() userService.deleteUser(auth.name) } @@ -68,7 +68,7 @@ class UserEndpoint( @RolesAllowed(Role.Names.ADMIN) fun getRolesBelow(): List { - val auth: Authentication = SecurityContextHolder.getContext().authentication + val auth: Authentication = getCurrentAuth() return roleService.getRolesBelowAuth(auth).map { it.roleName } } diff --git a/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt b/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt index 82ec4ab..e22bcda 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt @@ -6,9 +6,10 @@ import org.gameyfin.app.config.ConfigService import org.gameyfin.app.core.Role import org.gameyfin.app.core.Utils import org.gameyfin.app.core.events.* +import org.gameyfin.app.core.security.getCurrentAuth import org.gameyfin.app.games.entities.Image 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.UserUpdateDto 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.security.core.GrantedAuthority 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.UserDetails import org.springframework.security.core.userdetails.UserDetailsService @@ -77,7 +77,7 @@ class UserService( fun findByOidcProviderId(oidcProviderId: String): org.gameyfin.app.users.entities.User? = userRepository.findByOidcProviderId(oidcProviderId) - fun getAllUsers(): List { + fun getAllUsers(): List { return userRepository.findAll().map { u -> toUserInfo(u) } } @@ -93,8 +93,8 @@ class UserService( return userRepository.findByUsername(username) ?: throw UsernameNotFoundException("Unknown user '$username'") } - fun getUserInfo(): UserInfoDto { - val auth = SecurityContextHolder.getContext().authentication + fun getUserInfo(): UserInfoAdminDto { + val auth = getCurrentAuth() val principal = auth.principal if (principal is OidcUser) { @@ -238,7 +238,7 @@ class UserService( return RoleAssignmentResult.NO_ROLES_PROVIDED } - val currentUser = SecurityContextHolder.getContext().authentication + val currentUser = getCurrentAuth() val targetUser = getByUsernameNonNull(username) if (!canManage(targetUser)) { @@ -266,7 +266,7 @@ class UserService( } 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 targetUserLevel = roleService.getHighestRole(targetUser.roles).powerLevel return currentUserLevel > targetUserLevel @@ -285,8 +285,8 @@ class UserService( eventPublisher.publishEvent(AccountDeletedEvent(this, user, Utils.Companion.getBaseUrl())) } - fun toUserInfo(user: org.gameyfin.app.users.entities.User): UserInfoDto { - return UserInfoDto( + fun toUserInfo(user: org.gameyfin.app.users.entities.User): UserInfoAdminDto { + return UserInfoAdminDto( username = user.username, email = user.email, emailConfirmed = user.emailConfirmed, diff --git a/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoDto.kt b/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoAdminDto.kt similarity index 91% rename from app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoDto.kt rename to app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoAdminDto.kt index 26d3d96..5a93cde 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoDto.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoAdminDto.kt @@ -2,7 +2,7 @@ package org.gameyfin.app.users.dto import org.gameyfin.app.core.Role -data class UserInfoDto( +data class UserInfoAdminDto( val username: String, val managedBySso: Boolean, val email: String, diff --git a/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoUserDto.kt b/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoUserDto.kt new file mode 100644 index 0000000..5bbbfbc --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoUserDto.kt @@ -0,0 +1,7 @@ +package org.gameyfin.app.users.dto + +data class UserInfoUserDto( + val username: String, + val hasAvatar: Boolean, + val avatarId: Long? = null, +) \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/users/emailconfirmation/EmailConfirmationEndpoint.kt b/app/src/main/kotlin/org/gameyfin/app/users/emailconfirmation/EmailConfirmationEndpoint.kt index ddb1975..4a84f83 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/emailconfirmation/EmailConfirmationEndpoint.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/emailconfirmation/EmailConfirmationEndpoint.kt @@ -1,11 +1,10 @@ package org.gameyfin.app.users.emailconfirmation import com.vaadin.hilla.Endpoint -import org.gameyfin.app.users.UserService import jakarta.annotation.security.PermitAll +import org.gameyfin.app.core.security.getCurrentAuth import org.gameyfin.app.shared.token.TokenValidationResult -import org.springframework.security.core.Authentication -import org.springframework.security.core.context.SecurityContextHolder +import org.gameyfin.app.users.UserService @Endpoint class EmailConfirmationEndpoint( @@ -20,7 +19,7 @@ class EmailConfirmationEndpoint( @PermitAll fun resendEmailConfirmation() { - val auth: Authentication = SecurityContextHolder.getContext().authentication + val auth = getCurrentAuth() userService.getByUsername(auth.name)?.let { emailConfirmationService.resendEmailConfirmation(it) } diff --git a/app/src/main/kotlin/org/gameyfin/app/users/preferences/UserPreferencesService.kt b/app/src/main/kotlin/org/gameyfin/app/users/preferences/UserPreferencesService.kt index 963644e..617b5c8 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/preferences/UserPreferencesService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/preferences/UserPreferencesService.kt @@ -1,8 +1,8 @@ package org.gameyfin.app.users.preferences -import org.gameyfin.app.users.UserService 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 java.io.Serializable @@ -135,7 +135,7 @@ class UserPreferencesService( } private fun id(key: String): UserPreferenceKey { - val auth = SecurityContextHolder.getContext().authentication + val auth = getCurrentAuth() val user = userService.getByUsernameNonNull(auth.name) return UserPreferenceKey(key, user.id!!) } diff --git a/app/src/main/kotlin/org/gameyfin/app/users/registration/InvitationService.kt b/app/src/main/kotlin/org/gameyfin/app/users/registration/InvitationService.kt index b27602c..81d1974 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/registration/InvitationService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/registration/InvitationService.kt @@ -1,18 +1,17 @@ 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.UserInvitationEvent +import org.gameyfin.app.core.security.getCurrentAuth import org.gameyfin.app.shared.token.TokenDto 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.TokenType +import org.gameyfin.app.users.UserService import org.gameyfin.app.users.dto.UserRegistrationDto import org.gameyfin.app.users.enums.UserInvitationAcceptanceResult import org.springframework.context.ApplicationEventPublisher -import org.springframework.security.core.Authentication -import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Service @Service @@ -30,7 +29,7 @@ class InvitationService( if (userService.existsByEmail(email)) 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 payload = mapOf(EMAIL_KEY to email) val token = super.generateWithPayload(user, payload) @@ -45,7 +44,8 @@ class InvitationService( } 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 if (invitationToken.expired) return UserInvitationAcceptanceResult.TOKEN_EXPIRED