From 9540e1e5fe09292241830b0a9dd6a6b31af8f08c Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:15:01 +0200 Subject: [PATCH] Implement role mapping from SSO --- .../gameyfin/config/ConfigProperties.kt | 3 +- .../kotlin/de/grimsi/gameyfin/meta/Roles.kt | 8 +- .../grimsi/gameyfin/meta/SetupDataLoader.kt | 2 +- .../meta/security/AuthorityMapperConfig.kt | 16 ++++ .../meta/security/PasswordEncoderConfig.kt | 14 ++++ .../meta/{ => security}/SecurityConfig.kt | 24 ++---- .../meta/security/SessionRegistryConfig.kt | 14 ++++ .../SsoAuthenticationSuccessHandler.kt | 76 +++++++++++++++++++ .../{ => security}/SsoEnabledCondition.kt | 4 +- .../gameyfin/meta/{ => security}/WebConfig.kt | 2 +- .../de/grimsi/gameyfin/setup/SetupEndpoint.kt | 2 +- .../de/grimsi/gameyfin/setup/SetupService.kt | 2 +- .../de/grimsi/gameyfin/users/RoleService.kt | 51 ++++++++++++- .../de/grimsi/gameyfin/users/UserEndpoint.kt | 2 +- .../de/grimsi/gameyfin/users/UserService.kt | 31 +++++--- .../de/grimsi/gameyfin/users/entities/User.kt | 10 +-- .../users/persistence/UserRepository.kt | 2 + 17 files changed, 216 insertions(+), 47 deletions(-) create mode 100644 src/main/kotlin/de/grimsi/gameyfin/meta/security/AuthorityMapperConfig.kt create mode 100644 src/main/kotlin/de/grimsi/gameyfin/meta/security/PasswordEncoderConfig.kt rename src/main/kotlin/de/grimsi/gameyfin/meta/{ => security}/SecurityConfig.kt (88%) create mode 100644 src/main/kotlin/de/grimsi/gameyfin/meta/security/SessionRegistryConfig.kt create mode 100644 src/main/kotlin/de/grimsi/gameyfin/meta/security/SsoAuthenticationSuccessHandler.kt rename src/main/kotlin/de/grimsi/gameyfin/meta/{ => security}/SsoEnabledCondition.kt (97%) rename src/main/kotlin/de/grimsi/gameyfin/meta/{ => security}/WebConfig.kt (95%) diff --git a/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt b/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt index d32bbf3..d3932b9 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt @@ -122,7 +122,8 @@ sealed class ConfigProperties( data object SsoAutoRegisterNewUsers : ConfigProperties( Boolean::class, "sso.oidc.auto-register-new-users", - "Automatically create new users after registration" + "Automatically create new users after registration", + true ) /** Notifications */ diff --git a/src/main/kotlin/de/grimsi/gameyfin/meta/Roles.kt b/src/main/kotlin/de/grimsi/gameyfin/meta/Roles.kt index b575c5d..9eacefc 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/meta/Roles.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/meta/Roles.kt @@ -1,5 +1,7 @@ package de.grimsi.gameyfin.meta +import de.grimsi.gameyfin.users.RoleService.Companion.INTERNAL_ROLE_PREFIX + enum class Roles(val roleName: String) { SUPERADMIN(Names.SUPERADMIN), ADMIN(Names.ADMIN), @@ -8,9 +10,9 @@ enum class Roles(val roleName: String) { // necessary for the ability to use the Roles class in the @RolesAllowed annotation class Names { companion object { - const val SUPERADMIN = "ROLE_SUPERADMIN" - const val ADMIN = "ROLE_ADMIN" - const val USER = "ROLE_USER" + const val SUPERADMIN = "${INTERNAL_ROLE_PREFIX}SUPERADMIN" + const val ADMIN = "${INTERNAL_ROLE_PREFIX}ADMIN" + const val USER = "${INTERNAL_ROLE_PREFIX}USER" } } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/meta/SetupDataLoader.kt b/src/main/kotlin/de/grimsi/gameyfin/meta/SetupDataLoader.kt index a660ad4..23c6550 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/meta/SetupDataLoader.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/meta/SetupDataLoader.kt @@ -85,6 +85,6 @@ class SetupDataLoader( fun registerUserIfNotFound(user: User, role: Roles) { if (userService.existsByUsername(user.username)) return - userService.registerUser(user, role) + userService.registerOrUpdateUser(user, role) } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/meta/security/AuthorityMapperConfig.kt b/src/main/kotlin/de/grimsi/gameyfin/meta/security/AuthorityMapperConfig.kt new file mode 100644 index 0000000..bc80f37 --- /dev/null +++ b/src/main/kotlin/de/grimsi/gameyfin/meta/security/AuthorityMapperConfig.kt @@ -0,0 +1,16 @@ +package de.grimsi.gameyfin.meta.security + +import de.grimsi.gameyfin.users.RoleService +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper + +@Configuration +class AuthorityMapperConfig( + private val roleService: RoleService +) { + @Bean + fun userAuthoritiesMapper(): GrantedAuthoritiesMapper { + return GrantedAuthoritiesMapper { authorities -> roleService.extractGrantedAuthorities(authorities) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/meta/security/PasswordEncoderConfig.kt b/src/main/kotlin/de/grimsi/gameyfin/meta/security/PasswordEncoderConfig.kt new file mode 100644 index 0000000..c3352a7 --- /dev/null +++ b/src/main/kotlin/de/grimsi/gameyfin/meta/security/PasswordEncoderConfig.kt @@ -0,0 +1,14 @@ +package de.grimsi.gameyfin.meta.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder + +@Configuration +class PasswordEncoderConfig { + @Bean + fun passwordEncoder(): PasswordEncoder { + return BCryptPasswordEncoder() + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/meta/SecurityConfig.kt b/src/main/kotlin/de/grimsi/gameyfin/meta/security/SecurityConfig.kt similarity index 88% rename from src/main/kotlin/de/grimsi/gameyfin/meta/SecurityConfig.kt rename to src/main/kotlin/de/grimsi/gameyfin/meta/security/SecurityConfig.kt index 9a8f053..99cf108 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/meta/SecurityConfig.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/meta/security/SecurityConfig.kt @@ -1,4 +1,4 @@ -package de.grimsi.gameyfin.meta +package de.grimsi.gameyfin.meta.security import com.vaadin.flow.spring.security.VaadinWebSecurity import de.grimsi.gameyfin.config.ConfigProperties @@ -14,9 +14,6 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.core.session.SessionRegistry -import org.springframework.security.core.session.SessionRegistryImpl -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder -import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.oauth2.client.registration.ClientRegistration import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository @@ -27,7 +24,9 @@ import org.springframework.security.web.authentication.logout.HttpStatusReturnin @EnableWebSecurity class SecurityConfig( private val environment: Environment, - private val config: ConfigService + private val config: ConfigService, + private val ssoAuthenticationSuccessHandler: SsoAuthenticationSuccessHandler, + private val sessionRegistry: SessionRegistry ) : VaadinWebSecurity() { private val ssoProviderKey: String = "oidc" @@ -45,13 +44,16 @@ class SecurityConfig( sessionManagement .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(3) - .sessionRegistry(sessionRegistry()) + .sessionRegistry(sessionRegistry) } super.configure(http) if (config.getConfigValue(ConfigProperties.SsoEnabled) == true) { setOAuth2LoginPage(http, "/oauth2/authorization/$ssoProviderKey") + // Use custom success handler to handle user registration + http.oauth2Login { oauth2Login -> oauth2Login.successHandler(ssoAuthenticationSuccessHandler) } + // Prevent unnecessary redirects http.logout { logout -> logout.logoutSuccessHandler((HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))) } } else { setLoginView(http, "/login") @@ -67,11 +69,6 @@ class SecurityConfig( } } - @Bean - fun sessionRegistry(): SessionRegistry { - return SessionRegistryImpl() - } - // TODO: Maybe switch to a database-backed client registration repository? Not sure if worth it. @Bean @Conditional(SsoEnabledCondition::class) @@ -92,9 +89,4 @@ class SecurityConfig( return InMemoryClientRegistrationRepository(clientRegistration) } - - @Bean - fun passwordEncoder(): PasswordEncoder { - return BCryptPasswordEncoder() - } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/meta/security/SessionRegistryConfig.kt b/src/main/kotlin/de/grimsi/gameyfin/meta/security/SessionRegistryConfig.kt new file mode 100644 index 0000000..2a85c93 --- /dev/null +++ b/src/main/kotlin/de/grimsi/gameyfin/meta/security/SessionRegistryConfig.kt @@ -0,0 +1,14 @@ +package de.grimsi.gameyfin.meta.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.core.session.SessionRegistry +import org.springframework.security.core.session.SessionRegistryImpl + +@Configuration +class SessionRegistryConfig { + @Bean + fun sessionRegistry(): SessionRegistry { + return SessionRegistryImpl() + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/meta/security/SsoAuthenticationSuccessHandler.kt b/src/main/kotlin/de/grimsi/gameyfin/meta/security/SsoAuthenticationSuccessHandler.kt new file mode 100644 index 0000000..12cbb8f --- /dev/null +++ b/src/main/kotlin/de/grimsi/gameyfin/meta/security/SsoAuthenticationSuccessHandler.kt @@ -0,0 +1,76 @@ +package de.grimsi.gameyfin.meta.security + +import de.grimsi.gameyfin.config.ConfigProperties +import de.grimsi.gameyfin.config.ConfigService +import de.grimsi.gameyfin.config.MatchUsersBy +import de.grimsi.gameyfin.users.RoleService +import de.grimsi.gameyfin.users.UserService +import de.grimsi.gameyfin.users.entities.User +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.Authentication +import org.springframework.security.oauth2.core.oidc.user.OidcUser +import org.springframework.security.web.authentication.AuthenticationSuccessHandler +import org.springframework.stereotype.Component + +@Component +class SsoAuthenticationSuccessHandler( + private val userService: UserService, + private val roleService: RoleService, + private val config: ConfigService +) : AuthenticationSuccessHandler { + + override fun onAuthenticationSuccess( + request: HttpServletRequest, + response: HttpServletResponse, + authentication: Authentication + ) { + val oidcUser = authentication.principal as OidcUser + + // Check if user is already registered via SSO + var matchedUser = userService.findByOidcProviderId(oidcUser.subject) + + // If user is not registered via SSO, check if user is already registered by username or email + // This is meant to map existing users to SSO users + if (matchedUser == null) { + matchedUser = when (config.getConfigValue(ConfigProperties.SsoMatchExistingUsersBy)) { + MatchUsersBy.USERNAME -> { + userService.getByUsername(oidcUser.preferredUsername) + } + + MatchUsersBy.EMAIL -> { + userService.getByEmail(oidcUser.email) + } + + else -> { + throw IllegalStateException("Unknown 'match users by' configuration") + } + } + } + + // User could not be found in the database + if (matchedUser == null) { + + // Check if new user registration is enabled + if (config.getConfigValue(ConfigProperties.SsoAutoRegisterNewUsers) == false) { + response.sendRedirect("/") + return + } + + // Register new user + matchedUser = User(oidcUser) + } + // User was found in the database, but we still want to update the user's information + else { + matchedUser.username = oidcUser.preferredUsername + matchedUser.email = oidcUser.email + } + + val grantedAuthorities = roleService.extractGrantedAuthorities(oidcUser.authorities) + matchedUser.roles = roleService.authoritiesToRoles(grantedAuthorities) + userService.registerOrUpdateUser(matchedUser) + + response.sendRedirect("/") + return + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/meta/SsoEnabledCondition.kt b/src/main/kotlin/de/grimsi/gameyfin/meta/security/SsoEnabledCondition.kt similarity index 97% rename from src/main/kotlin/de/grimsi/gameyfin/meta/SsoEnabledCondition.kt rename to src/main/kotlin/de/grimsi/gameyfin/meta/security/SsoEnabledCondition.kt index 6bcaa99..3c99a69 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/meta/SsoEnabledCondition.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/meta/security/SsoEnabledCondition.kt @@ -1,4 +1,4 @@ -package de.grimsi.gameyfin.meta +package de.grimsi.gameyfin.meta.security import de.grimsi.gameyfin.config.ConfigProperties import org.springframework.context.annotation.Condition @@ -32,7 +32,7 @@ class SsoEnabledCondition : Condition { } catch (_: Exception) { return false } - + return false } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/meta/WebConfig.kt b/src/main/kotlin/de/grimsi/gameyfin/meta/security/WebConfig.kt similarity index 95% rename from src/main/kotlin/de/grimsi/gameyfin/meta/WebConfig.kt rename to src/main/kotlin/de/grimsi/gameyfin/meta/security/WebConfig.kt index 61e3b62..d47a4bb 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/meta/WebConfig.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/meta/security/WebConfig.kt @@ -1,4 +1,4 @@ -package de.grimsi.gameyfin.meta +package de.grimsi.gameyfin.meta.security import de.grimsi.gameyfin.meta.annotations.DynamicAccessInterceptor import de.grimsi.gameyfin.meta.development.DelayInterceptor diff --git a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupEndpoint.kt index e9c0be1..e9536bc 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupEndpoint.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupEndpoint.kt @@ -29,7 +29,7 @@ class SetupEndpoint( username = superAdminRegistration.username, password = superAdminRegistration.password, email = superAdminRegistration.email, - roles = listOf(roleService.toRole(Roles.SUPERADMIN)) + roles = setOf(roleService.toRole(Roles.SUPERADMIN)) ) val superAdmin = setupService.createInitialAdminUser(user) diff --git a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt index d0cb2bb..b73f9e5 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/setup/SetupService.kt @@ -25,6 +25,6 @@ class SetupService( * Creates the initial user with Super-Admin permissions */ fun createInitialAdminUser(superAdmin: User): User { - return userService.registerUser(superAdmin, Roles.SUPERADMIN) + return userService.registerOrUpdateUser(superAdmin, Roles.SUPERADMIN) } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt b/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt index 37f1cb4..97a253c 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt @@ -4,6 +4,9 @@ import de.grimsi.gameyfin.meta.Roles import de.grimsi.gameyfin.users.entities.Role import de.grimsi.gameyfin.users.persistence.RoleRepository import jakarta.transaction.Transactional +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority import org.springframework.stereotype.Service @Service @@ -11,6 +14,12 @@ import org.springframework.stereotype.Service class RoleService( private val roleRepository: RoleRepository ) { + + companion object { + const val SSO_ROLE_PREFIX = "GAMEYFIN_" + const val INTERNAL_ROLE_PREFIX = "ROLE_" + } + /** * @return the number of registered users with a given role * @return 0 if a role does not exist @@ -20,12 +29,50 @@ class RoleService( return r.users.size } - fun toRoles(roles: Collection): List { - return roles.mapNotNull { r -> roleRepository.findByRolename(r.roleName) } + fun toRoles(roles: Collection): Set { + return roles.mapNotNull { r -> roleRepository.findByRolename(r.roleName) }.toSet() } fun toRole(role: Roles): Role { return roleRepository.findByRolename(role.roleName) ?: throw RuntimeException("Role ${role.roleName} does not exist") } + + fun authoritiesToRoles(authorities: Collection): Set { + return authorities.mapNotNull { a -> roleRepository.findByRolename(a.authority) }.toSet() + } + + /** + * Extracts granted authorities from a collection of granted authorities. + * Also converts SSO roles to internal roles. + * SSO roles are assumed to be prefixed with "GAMEYFIN_" to avoid collision with other SSO apps managed by the same provider. + * Internal roles are prefixed with "ROLE_" as per Spring Security conventions. + * Ignore any authorities that do not start with "GAMEYFIN_". + * + * @return filtered and mapped collection of granted authorities + */ + fun extractGrantedAuthorities(authorities: Collection): Collection { + val mappedAuthorities = authorities.asSequence() + .filterIsInstance() + .flatMap { oidcUserAuthority -> + val userInfo = oidcUserAuthority.userInfo + val roles = userInfo.getClaim>("roles") + roles.asSequence().mapNotNull { + if (it.startsWith(SSO_ROLE_PREFIX)) SimpleGrantedAuthority( + it.replace( + SSO_ROLE_PREFIX, + INTERNAL_ROLE_PREFIX + ) + ) + else null + } + } + .toSet() + + if (mappedAuthorities.isEmpty()) { + mappedAuthorities.plus(SimpleGrantedAuthority(Roles.Names.USER)) + } + + return mappedAuthorities + } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt index e9acd38..6e1b439 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt @@ -63,6 +63,6 @@ class UserEndpoint( email = registration.email ) - return userService.registerUser(user, roles) + return userService.registerOrUpdateUser(user, roles) } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt b/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt index 8ce989d..fc4a4b5 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt @@ -16,7 +16,6 @@ import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.security.oauth2.core.oidc.user.OidcUser import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile import java.io.InputStream @@ -46,19 +45,23 @@ class UserService( ) } - fun existsByUsername(username: String): Boolean = userRepository.findByUsername(username) != null + fun existsByUsername(username: String): Boolean = userRepository.existsByUsername(username) + + fun findByOidcProviderId(oidcProviderId: String): User? = userRepository.findByOidcProviderId(oidcProviderId) fun getAllUsers(): List { return userRepository.findAll().map { u -> toUserInfo(u) } } + fun getByEmail(email: String): User? { + return userRepository.findByEmail(email) + } + + fun getByUsername(username: String): User? { + return userRepository.findByUsername(username) + } + fun getUserInfo(auth: Authentication): UserInfoDto { - val principal = auth.principal - - if (principal is OidcUser) { - return toUserInfo(User(principal)) - } - val user = userByUsername(auth.name) return toUserInfo(user) } @@ -94,12 +97,16 @@ class UserService( userRepository.save(user) } - fun registerUser(user: User, role: Roles): User { - return registerUser(user, listOf(role)) + fun registerOrUpdateUser(user: User): User { + return userRepository.save(user) } - fun registerUser(user: User, roles: List): User { - user.password = passwordEncoder.encode(user.password) + fun registerOrUpdateUser(user: User, role: Roles): User { + return registerOrUpdateUser(user, listOf(role)) + } + + fun registerOrUpdateUser(user: User, roles: List): User { + user.password?.let { user.password = passwordEncoder.encode(it) } user.roles = roleService.toRoles(roles) return userRepository.save(user) } diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt b/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt index beae61f..f3a766d 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt @@ -1,6 +1,5 @@ package de.grimsi.gameyfin.users.entities -import de.grimsi.gameyfin.meta.Roles import jakarta.annotation.Nullable import jakarta.persistence.* import jakarta.validation.constraints.NotNull @@ -40,15 +39,14 @@ class User( joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")], inverseJoinColumns = [JoinColumn(name = "role_id", referencedColumnName = "id")] ) - var roles: Collection = emptyList() + var roles: Set = emptySet() ) { constructor(oidcUser: OidcUser) : this( username = oidcUser.preferredUsername, email = oidcUser.email, + email_confirmed = true, + enabled = true, oidcProviderId = oidcUser.subject - ) { - // FIXME: Implement role mapping from OIDC provider - this.roles = listOf(Role(Roles.ADMIN.roleName)) - } + ) } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/persistence/UserRepository.kt b/src/main/kotlin/de/grimsi/gameyfin/users/persistence/UserRepository.kt index 53913e3..fd3d6a9 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/persistence/UserRepository.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/persistence/UserRepository.kt @@ -4,6 +4,8 @@ import de.grimsi.gameyfin.users.entities.User import org.springframework.data.jpa.repository.JpaRepository interface UserRepository : JpaRepository { + fun existsByUsername(userName: String): Boolean fun findByUsername(userName: String): User? + fun findByEmail(email: String): User? fun findByOidcProviderId(oidcProviderId: String): User? } \ No newline at end of file