From e47ab8405f0ed52b71b88608129f3d147a8bed6e Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Fri, 27 Sep 2024 09:34:52 +0200 Subject: [PATCH] Implemented email confirmation flow Implemented user registration confirmation flow --- .../administration/ProfileManagement.tsx | 25 +++++- src/main/frontend/routes.tsx | 4 + .../frontend/views/EmailConfirmationView.tsx | 76 +++++++++++++++++++ src/main/frontend/views/MainLayout.tsx | 19 ++++- .../grimsi/gameyfin/core/SetupDataLoader.kt | 8 +- .../de/grimsi/gameyfin/core/events/Events.kt | 4 + .../gameyfin/core/security/SecurityConfig.kt | 2 +- .../gameyfin/messages/MessageService.kt | 26 ++++++- .../de/grimsi/gameyfin/shared/token/Token.kt | 11 +-- .../gameyfin/shared/token/TokenService.kt | 17 ++++- .../grimsi/gameyfin/shared/token/TokenType.kt | 2 +- .../shared/token/TokenTypeUserType.kt | 16 +++- .../de/grimsi/gameyfin/users/UserService.kt | 10 +++ .../EmailConfirmationEndpoint.kt | 28 +++++++ .../EmailConfirmationService.kt | 54 +++++++++++++ .../de/grimsi/gameyfin/users/entities/User.kt | 2 +- .../PasswordResetEndpoint.kt | 3 +- .../PasswordResetService.kt | 8 +- .../RegistrationEndpoint.kt | 3 +- 19 files changed, 283 insertions(+), 35 deletions(-) create mode 100644 src/main/frontend/views/EmailConfirmationView.tsx create mode 100644 src/main/kotlin/de/grimsi/gameyfin/users/emailconfirmation/EmailConfirmationEndpoint.kt create mode 100644 src/main/kotlin/de/grimsi/gameyfin/users/emailconfirmation/EmailConfirmationService.kt rename src/main/kotlin/de/grimsi/gameyfin/users/{ => passwordreset}/PasswordResetEndpoint.kt (91%) rename src/main/kotlin/de/grimsi/gameyfin/users/{ => passwordreset}/PasswordResetService.kt (92%) rename src/main/kotlin/de/grimsi/gameyfin/users/{ => registration}/RegistrationEndpoint.kt (90%) diff --git a/src/main/frontend/components/administration/ProfileManagement.tsx b/src/main/frontend/components/administration/ProfileManagement.tsx index d7b9b7b..d77a363 100644 --- a/src/main/frontend/components/administration/ProfileManagement.tsx +++ b/src/main/frontend/components/administration/ProfileManagement.tsx @@ -2,12 +2,12 @@ import Section from "Frontend/components/general/Section"; import Input from "Frontend/components/general/Input"; import {Button, Input as NextUiInput, Tooltip} from "@nextui-org/react"; import {Form, Formik} from "formik"; -import {Check, Info, Trash} from "@phosphor-icons/react"; +import {ArrowCounterClockwise, Check, Info, Trash} from "@phosphor-icons/react"; import React, {useEffect, useState} from "react"; import {useAuth} from "Frontend/util/auth"; import * as Yup from "yup"; import UserUpdateDto from "Frontend/generated/de/grimsi/gameyfin/users/dto/UserUpdateDto"; -import {UserEndpoint} from "Frontend/generated/endpoints"; +import {EmailConfirmationEndpoint, UserEndpoint} from "Frontend/generated/endpoints"; import {SmallInfoField} from "Frontend/components/general/SmallInfoField"; import {toast} from "sonner"; import {removeAvatar, uploadAvatar} from "Frontend/endpoints/AvatarEndpoint"; @@ -120,8 +120,25 @@ export default function ProfileManagement() {
- +
+ + {auth.state.user?.emailConfirmed === false && + + + + } +
diff --git a/src/main/frontend/routes.tsx b/src/main/frontend/routes.tsx index f7b3cdc..fca568f 100644 --- a/src/main/frontend/routes.tsx +++ b/src/main/frontend/routes.tsx @@ -15,6 +15,7 @@ import {ProfileView} from "Frontend/views/ProfileView"; import {MessageManagement} from "Frontend/components/administration/MessageManagement"; import {LogManagement} from "Frontend/components/administration/LogManagement"; import PasswordResetView from "Frontend/views/PasswordResetView"; +import EmailConfirmationView from "Frontend/views/EmailConfirmationView"; export const routes = protectRoutes([ { @@ -57,6 +58,9 @@ export const routes = protectRoutes([ }, { path: '/reset-password', element: , handle: {requiresLogin: false} + }, + { + path: '/confirm-email', element: , handle: {requiresLogin: true} } ], } diff --git a/src/main/frontend/views/EmailConfirmationView.tsx b/src/main/frontend/views/EmailConfirmationView.tsx new file mode 100644 index 0000000..ced45bd --- /dev/null +++ b/src/main/frontend/views/EmailConfirmationView.tsx @@ -0,0 +1,76 @@ +import {Card, CardBody, CardHeader} from "@nextui-org/react"; +import {useNavigate, useSearchParams} from "react-router-dom"; +import React, {useEffect, useState} from "react"; +import {CheckCircle, Warning, WarningCircle} from "@phosphor-icons/react"; +import TokenValidationResult from "Frontend/generated/de/grimsi/gameyfin/shared/token/TokenValidationResult"; +import {EmailConfirmationEndpoint} from "Frontend/generated/endpoints"; +import {useAuth} from "Frontend/util/auth"; + +export default function EmailConfirmationView() { + const auth = useAuth(); + const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + const [validationResult, setValidationResult] = useState(TokenValidationResult.INVALID); + + useEffect(() => { + if (auth.state.user?.emailConfirmed === true) { + navigate("/"); + } + }, []); + + useEffect(() => { + let token = searchParams.get("token"); + if (token) confirmEmail(token).then((result) => setValidationResult(result)); + }, [searchParams]); + + async function confirmEmail(token: string): Promise { + let result = await EmailConfirmationEndpoint.confirmEmail(token) as TokenValidationResult; + + if (result === TokenValidationResult.VALID) { + setTimeout(() => window.location.reload(), 5000); + } + + return result; + } + + return ( +
+ + + Gameyfin Logo + + + {validationResult === TokenValidationResult.VALID ? +
+ +

+ Email confirmed
+ You will be redirected shortly +

+
+ : validationResult === TokenValidationResult.EXPIRED ? +
+ +

+ Expired token
+ Please request a new one +

+
+ : +
+ +

+ Invalid token
+ Please try again +

+
+ } +
+
+
+ ); +} \ No newline at end of file diff --git a/src/main/frontend/views/MainLayout.tsx b/src/main/frontend/views/MainLayout.tsx index 10f8507..64f2023 100644 --- a/src/main/frontend/views/MainLayout.tsx +++ b/src/main/frontend/views/MainLayout.tsx @@ -5,13 +5,17 @@ import {Divider, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem} from "@ne import GameyfinLogo from "Frontend/components/theming/GameyfinLogo"; import * as PackageJson from "../../../../package.json"; import {Outlet, useNavigate} from "react-router-dom"; +import {useAuth} from "Frontend/util/auth"; export default function MainLayout() { - const currentTitle = `Gameyfin - ${useRouteMetadata()?.title}` ?? 'Gameyfin'; - useEffect(() => { - document.title = currentTitle; - }, [currentTitle]); const navigate = useNavigate(); + const auth = useAuth(); + const routeMetadata = useRouteMetadata(); + + useEffect(() => { + let newTitle = `Gameyfin - ${routeMetadata?.title}` ?? 'Gameyfin'; + window.addEventListener('popstate', () => document.title = newTitle); + }, []); return (
@@ -21,6 +25,13 @@ export default function MainLayout() { + {auth.state.user?.emailConfirmed === false ? + + Please confirm your email + + : + "" + } diff --git a/src/main/kotlin/de/grimsi/gameyfin/core/SetupDataLoader.kt b/src/main/kotlin/de/grimsi/gameyfin/core/SetupDataLoader.kt index 2c54f83..899412d 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/core/SetupDataLoader.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/core/SetupDataLoader.kt @@ -70,7 +70,9 @@ class SetupDataLoader( val superadmin = User( username = "admin", password = "admin", - email = "admin@gameyfin.org" + email = "admin@gameyfin.org", + emailConfirmed = true, + enabled = true ) registerUserIfNotFound(superadmin, Roles.SUPERADMIN) @@ -78,7 +80,9 @@ class SetupDataLoader( val user = User( username = "user", password = "user", - email = "user@gameyfin.org" + email = "user@gameyfin.org", + emailConfirmed = true, + enabled = true ) registerUserIfNotFound(user, Roles.USER) diff --git a/src/main/kotlin/de/grimsi/gameyfin/core/events/Events.kt b/src/main/kotlin/de/grimsi/gameyfin/core/events/Events.kt index 40e90fb..fe25f95 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/core/events/Events.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/core/events/Events.kt @@ -1,6 +1,7 @@ package de.grimsi.gameyfin.core.events import de.grimsi.gameyfin.shared.token.Token +import de.grimsi.gameyfin.shared.token.TokenType.EmailConfirmation import de.grimsi.gameyfin.shared.token.TokenType.PasswordReset import de.grimsi.gameyfin.users.entities.User import org.springframework.context.ApplicationEvent @@ -11,6 +12,9 @@ class UserRegistrationWaitingForApprovalEvent(source: Any, val newUser: User) : class UserRegistrationEvent(source: Any, val newUser: User, val baseUrl: String) : ApplicationEvent(source) +class EmailNeedsConfirmationEvent(source: Any, val token: Token, val baseUrl: String) : + ApplicationEvent(source) + class RegistrationAttemptWithExistingEmailEvent(source: Any, val existingUser: User, val baseUrl: String) : ApplicationEvent(source) diff --git a/src/main/kotlin/de/grimsi/gameyfin/core/security/SecurityConfig.kt b/src/main/kotlin/de/grimsi/gameyfin/core/security/SecurityConfig.kt index cb41a94..9cc497b 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/core/security/SecurityConfig.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/core/security/SecurityConfig.kt @@ -37,6 +37,7 @@ class SecurityConfig( http.authorizeHttpRequests { auth: AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry -> auth.requestMatchers("/setup").permitAll() .requestMatchers("/reset-password").permitAll() + .requestMatchers("/accept-invitation").permitAll() .requestMatchers("/public/**").permitAll() .requestMatchers("/images/**").permitAll() } @@ -70,7 +71,6 @@ class SecurityConfig( } } - // TODO: Maybe switch to a database-backed client registration repository? Not sure if worth it. @Bean @Conditional(SsoEnabledCondition::class) fun clientRegistrationRepository(): ClientRegistrationRepository? { diff --git a/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt b/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt index 2cc65b7..aec86a3 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt @@ -1,9 +1,6 @@ package de.grimsi.gameyfin.messages -import de.grimsi.gameyfin.core.events.PasswordResetRequestEvent -import de.grimsi.gameyfin.core.events.RegistrationAttemptWithExistingEmailEvent -import de.grimsi.gameyfin.core.events.UserRegistrationEvent -import de.grimsi.gameyfin.core.events.UserRegistrationWaitingForApprovalEvent +import de.grimsi.gameyfin.core.events.* import de.grimsi.gameyfin.messages.providers.AbstractMessageProvider import de.grimsi.gameyfin.messages.templates.MessageTemplateService import de.grimsi.gameyfin.messages.templates.MessageTemplates @@ -158,4 +155,25 @@ class MessageService( mapOf("username" to user.username, "passwordResetLink" to event.baseUrl) ) } + + @Async + @EventListener(EmailNeedsConfirmationEvent::class) + fun onEmailNeedsConfirmation(event: EmailNeedsConfirmationEvent) { + + if (!enabled) { + log.error { "No notification provider available, can't send email confirmation message" } + return + } + + log.info { "Sending email confirmation notification" } + + val user = event.token.user + val confirmationLink = event.baseUrl + "/confirm-email?token=${event.token.secret}" + sendNotification( + user.email, + "[Gameyfin] Email Confirmation", + MessageTemplates.EmailConfirmation, + mapOf("username" to user.username, "confirmationLink" to confirmationLink) + ) + } } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/shared/token/Token.kt b/src/main/kotlin/de/grimsi/gameyfin/shared/token/Token.kt index 85201e9..ff4ef41 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/shared/token/Token.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/shared/token/Token.kt @@ -2,15 +2,11 @@ package de.grimsi.gameyfin.shared.token import de.grimsi.gameyfin.core.security.EncryptionConverter import de.grimsi.gameyfin.users.entities.User -import jakarta.persistence.Convert -import jakarta.persistence.Entity -import jakarta.persistence.FetchType -import jakarta.persistence.Id -import jakarta.persistence.OneToOne +import jakarta.persistence.* import org.hibernate.annotations.CreationTimestamp import org.hibernate.annotations.Type import java.time.Instant -import java.util.UUID +import java.util.* import kotlin.time.toJavaDuration @Entity @@ -29,5 +25,6 @@ class Token( val createdOn: Instant? = null ) { val expired: Boolean - get() = createdOn?.plus(type.expiration.toJavaDuration())!!.isBefore(Instant.now()) + get() = type.expiration.isFinite() && + createdOn?.plus(type.expiration.toJavaDuration())!!.isBefore(Instant.now()) } \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenService.kt b/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenService.kt index ffbc95d..85df0fb 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenService.kt @@ -2,6 +2,7 @@ package de.grimsi.gameyfin.shared.token import de.grimsi.gameyfin.users.entities.User import io.github.oshai.kotlinlogging.KotlinLogging +import jakarta.transaction.Transactional abstract class TokenService( private val type: T, @@ -10,6 +11,7 @@ abstract class TokenService( private val log = KotlinLogging.logger {} + @Transactional open fun generate(user: User): Token { val token = Token( user = user, @@ -24,7 +26,8 @@ abstract class TokenService( return tokenRepository.save(token) } - fun get(secret: String, type: T): Token? { + @Transactional + open fun get(secret: String, type: T): Token? { val token = tokenRepository.findBySecret(secret) ?: return null return if (token.type == type) { @@ -36,11 +39,17 @@ abstract class TokenService( } } - fun delete(token: Token) { - tokenRepository.delete(token) + @Transactional + open fun delete(token: Token) { + try { + tokenRepository.delete(token) + } catch (_: Exception) { + log.warn { "Token '$token' has already been deleted" } + } } - fun validate(secret: String): TokenValidationResult { + @Transactional + open fun validate(secret: String): TokenValidationResult { val token = tokenRepository.findBySecret(secret) ?: return TokenValidationResult.INVALID return if (token.expired) TokenValidationResult.EXPIRED else TokenValidationResult.VALID } diff --git a/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenType.kt b/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenType.kt index bbc916a..57bf6b9 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenType.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenType.kt @@ -8,6 +8,6 @@ sealed class TokenType( val expiration: Duration ) { data object PasswordReset : TokenType("password-reset", 15.minutes) - data object EmailVerification : TokenType("email-verification", Duration.INFINITE) + data object EmailConfirmation : TokenType("email-verification", Duration.INFINITE) data object Invitation : TokenType("invitation", Duration.INFINITE) } diff --git a/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenTypeUserType.kt b/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenTypeUserType.kt index 50140d5..c6ebfd5 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenTypeUserType.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenTypeUserType.kt @@ -14,7 +14,11 @@ class TokenTypeUserType : UserType { override fun returnedClass(): Class = TokenType::class.java - override fun equals(x: TokenType, y: TokenType): Boolean = x.key == y.key + override fun equals(x: TokenType?, y: TokenType?): Boolean { + if (x === y) return true + if (x == null || y == null) return false + return x.key == y.key + } override fun hashCode(x: TokenType): Int = x.key.hashCode() @@ -52,5 +56,13 @@ class TokenTypeUserType : UserType { override fun disassemble(value: TokenType): Serializable = value.key - override fun assemble(cached: Serializable, owner: Any?): TokenType = cached as TokenType + override fun assemble(cached: Serializable, owner: Any?): TokenType { + val key = cached as? String ?: throw IllegalArgumentException("Invalid cached value: $cached") + val tokenTypeClass = TokenType::class + + return tokenTypeClass.sealedSubclasses + .map { it.objectInstance ?: it.createInstance() } + .firstOrNull { it.key == key } + ?: throw IllegalArgumentException("Unknown TokenType: $key") + } } \ 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 61540ad..6c67b8f 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt @@ -4,12 +4,14 @@ import de.grimsi.gameyfin.config.ConfigProperties import de.grimsi.gameyfin.config.ConfigService import de.grimsi.gameyfin.core.Roles import de.grimsi.gameyfin.core.Utils +import de.grimsi.gameyfin.core.events.EmailNeedsConfirmationEvent import de.grimsi.gameyfin.core.events.RegistrationAttemptWithExistingEmailEvent import de.grimsi.gameyfin.core.events.UserRegistrationEvent import de.grimsi.gameyfin.core.events.UserRegistrationWaitingForApprovalEvent import de.grimsi.gameyfin.users.dto.UserInfoDto import de.grimsi.gameyfin.users.dto.UserRegistrationDto import de.grimsi.gameyfin.users.dto.UserUpdateDto +import de.grimsi.gameyfin.users.emailconfirmation.EmailConfirmationService import de.grimsi.gameyfin.users.entities.Avatar import de.grimsi.gameyfin.users.entities.Role import de.grimsi.gameyfin.users.entities.User @@ -39,6 +41,7 @@ class UserService( private val passwordEncoder: PasswordEncoder, private val roleService: RoleService, private val sessionService: SessionService, + private val emailConfirmationService: EmailConfirmationService, private val config: ConfigService, private val eventPublisher: ApplicationEventPublisher ) : UserDetailsService { @@ -168,6 +171,11 @@ class UserService( } else { eventPublisher.publishEvent(UserRegistrationEvent(this, user, Utils.getBaseUrl())) } + + if (!user.emailConfirmed) { + val token = emailConfirmationService.generate(user) + eventPublisher.publishEvent(EmailNeedsConfirmationEvent(this, token, Utils.getBaseUrl())) + } } fun updateUser(username: String, updates: UserUpdateDto) { @@ -183,6 +191,8 @@ class UserService( updates.email?.let { user.email = it user.emailConfirmed = false + val token = emailConfirmationService.generate(user) + eventPublisher.publishEvent(EmailNeedsConfirmationEvent(this, token, Utils.getBaseUrl())) } userRepository.save(user) diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/emailconfirmation/EmailConfirmationEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/users/emailconfirmation/EmailConfirmationEndpoint.kt new file mode 100644 index 0000000..8802082 --- /dev/null +++ b/src/main/kotlin/de/grimsi/gameyfin/users/emailconfirmation/EmailConfirmationEndpoint.kt @@ -0,0 +1,28 @@ +package de.grimsi.gameyfin.users.emailconfirmation + +import com.vaadin.hilla.Endpoint +import de.grimsi.gameyfin.shared.token.TokenValidationResult +import de.grimsi.gameyfin.users.UserService +import jakarta.annotation.security.PermitAll +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContextHolder + +@Endpoint +class EmailConfirmationEndpoint( + private val emailConfirmationService: EmailConfirmationService, + private val userService: UserService +) { + + @PermitAll + fun confirmEmail(token: String): TokenValidationResult { + return emailConfirmationService.confirmEmail(token) + } + + @PermitAll + fun resendEmailConfirmation() { + val auth: Authentication = SecurityContextHolder.getContext().authentication + userService.getByUsername(auth.name)?.let { + emailConfirmationService.resendEmailConfirmation(it) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/emailconfirmation/EmailConfirmationService.kt b/src/main/kotlin/de/grimsi/gameyfin/users/emailconfirmation/EmailConfirmationService.kt new file mode 100644 index 0000000..e97fc96 --- /dev/null +++ b/src/main/kotlin/de/grimsi/gameyfin/users/emailconfirmation/EmailConfirmationService.kt @@ -0,0 +1,54 @@ +package de.grimsi.gameyfin.users.emailconfirmation + +import de.grimsi.gameyfin.core.Utils +import de.grimsi.gameyfin.core.events.EmailNeedsConfirmationEvent +import de.grimsi.gameyfin.shared.token.TokenRepository +import de.grimsi.gameyfin.shared.token.TokenService +import de.grimsi.gameyfin.shared.token.TokenType.EmailConfirmation +import de.grimsi.gameyfin.shared.token.TokenValidationResult +import de.grimsi.gameyfin.users.entities.User +import de.grimsi.gameyfin.users.persistence.UserRepository +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.context.ApplicationEventPublisher +import org.springframework.stereotype.Service + +@Service +class EmailConfirmationService( + tokenRepository: TokenRepository, + private val userRepository: UserRepository, + private val eventPublisher: ApplicationEventPublisher +) : TokenService(EmailConfirmation, tokenRepository) { + + val log: KLogger = KotlinLogging.logger {} + + fun confirmEmail(secret: String): TokenValidationResult { + val emailConfirmationToken = get(secret, EmailConfirmation) + ?: return TokenValidationResult.INVALID + + if (emailConfirmationToken.expired) { + return TokenValidationResult.EXPIRED + } + + val user = emailConfirmationToken.user + confirmEmail(user) + delete(emailConfirmationToken) + + return TokenValidationResult.VALID + } + + fun resendEmailConfirmation(user: User) { + if (user.emailConfirmed) { + log.error { "User '${user.username}' has already confirmed their email address" } + return + } + + val token = generate(user) + eventPublisher.publishEvent(EmailNeedsConfirmationEvent(user, token, Utils.getBaseUrl())) + } + + private fun confirmEmail(user: User) { + user.emailConfirmed = true + userRepository.save(user) + } +} \ No newline at end of file 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 c8d538a..2d3d47d 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt @@ -29,7 +29,7 @@ class User( var emailConfirmed: Boolean = false, - var enabled: Boolean = true, + var enabled: Boolean = false, @Embedded @Nullable diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/users/passwordreset/PasswordResetEndpoint.kt similarity index 91% rename from src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetEndpoint.kt rename to src/main/kotlin/de/grimsi/gameyfin/users/passwordreset/PasswordResetEndpoint.kt index 31cfab1..f6b89f5 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetEndpoint.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/passwordreset/PasswordResetEndpoint.kt @@ -1,10 +1,11 @@ -package de.grimsi.gameyfin.users +package de.grimsi.gameyfin.users.passwordreset import com.vaadin.flow.server.auth.AnonymousAllowed import com.vaadin.hilla.Endpoint import de.grimsi.gameyfin.core.Roles import de.grimsi.gameyfin.shared.token.TokenDto import de.grimsi.gameyfin.shared.token.TokenValidationResult +import de.grimsi.gameyfin.users.UserService import jakarta.annotation.security.RolesAllowed @Endpoint diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetService.kt b/src/main/kotlin/de/grimsi/gameyfin/users/passwordreset/PasswordResetService.kt similarity index 92% rename from src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetService.kt rename to src/main/kotlin/de/grimsi/gameyfin/users/passwordreset/PasswordResetService.kt index e85eba4..0526370 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetService.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/passwordreset/PasswordResetService.kt @@ -1,10 +1,12 @@ -package de.grimsi.gameyfin.users +package de.grimsi.gameyfin.users.passwordreset import de.grimsi.gameyfin.core.Utils import de.grimsi.gameyfin.core.events.PasswordResetRequestEvent import de.grimsi.gameyfin.messages.MessageService import de.grimsi.gameyfin.shared.token.* import de.grimsi.gameyfin.shared.token.TokenType.PasswordReset +import de.grimsi.gameyfin.users.SessionService +import de.grimsi.gameyfin.users.UserService import de.grimsi.gameyfin.users.entities.User import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.context.ApplicationEventPublisher @@ -89,8 +91,8 @@ class PasswordResetService( Thread.sleep(secureRandom.nextLong(1024)) } - fun resetPassword(token: String, newPassword: String): TokenValidationResult { - val passwordResetToken = get(token, PasswordReset) + fun resetPassword(secret: String, newPassword: String): TokenValidationResult { + val passwordResetToken = get(secret, PasswordReset) ?: return TokenValidationResult.INVALID if (passwordResetToken.expired) { diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/RegistrationEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/users/registration/RegistrationEndpoint.kt similarity index 90% rename from src/main/kotlin/de/grimsi/gameyfin/users/RegistrationEndpoint.kt rename to src/main/kotlin/de/grimsi/gameyfin/users/registration/RegistrationEndpoint.kt index 786f2bb..46dec4a 100644 --- a/src/main/kotlin/de/grimsi/gameyfin/users/RegistrationEndpoint.kt +++ b/src/main/kotlin/de/grimsi/gameyfin/users/registration/RegistrationEndpoint.kt @@ -1,8 +1,9 @@ -package de.grimsi.gameyfin.users +package de.grimsi.gameyfin.users.registration import com.vaadin.flow.server.auth.AnonymousAllowed import com.vaadin.hilla.Endpoint import de.grimsi.gameyfin.core.Roles +import de.grimsi.gameyfin.users.UserService import de.grimsi.gameyfin.users.dto.UserRegistrationDto import jakarta.annotation.security.RolesAllowed