mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Implemented email confirmation flow
Implemented user registration confirmation flow
This commit is contained in:
@@ -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() {
|
||||
<Section title="Personal information"/>
|
||||
<Input name="username" label="Username" type="text" autocomplete="username"
|
||||
isDisabled={auth.state.user?.managedBySso}/>
|
||||
<Input name="email" label="Email" type="email" autocomplete="email"
|
||||
isDisabled={auth.state.user?.managedBySso}/>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input name="email" label="Email" type="email" autocomplete="email"
|
||||
isDisabled={auth.state.user?.managedBySso}/>
|
||||
{auth.state.user?.emailConfirmed === false &&
|
||||
<Tooltip content="Resend email confirmation message">
|
||||
<Button isIconOnly
|
||||
onPress={() => {
|
||||
EmailConfirmationEndpoint.resendEmailConfirmation().then(
|
||||
() => toast.success("You will receive an email shortly")
|
||||
)
|
||||
}}
|
||||
variant="ghost"
|
||||
className="size-14"
|
||||
>
|
||||
<ArrowCounterClockwise size={26}/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
<Section title="Security"/>
|
||||
<Input name="newPassword" label="New Password" type="password"
|
||||
autocomplete="new-password" isDisabled={auth.state.user?.managedBySso}/>
|
||||
|
||||
@@ -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: <PasswordResetView/>, handle: {requiresLogin: false}
|
||||
},
|
||||
{
|
||||
path: '/confirm-email', element: <EmailConfirmationView/>, handle: {requiresLogin: true}
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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>(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<TokenValidationResult> {
|
||||
let result = await EmailConfirmationEndpoint.confirmEmail(token) as TokenValidationResult;
|
||||
|
||||
if (result === TokenValidationResult.VALID) {
|
||||
setTimeout(() => window.location.reload(), 5000);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-grow items-center justify-center size-full gradient-primary">
|
||||
<Card className="p-4 min-w-[468px]">
|
||||
<CardHeader className="mb-4">
|
||||
<img
|
||||
className="h-28 w-full content-center"
|
||||
src="/images/Logo.svg"
|
||||
alt="Gameyfin Logo"
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardBody className="flex flex-row justify-center">
|
||||
{validationResult === TokenValidationResult.VALID ?
|
||||
<div className="flex flex-row items-center gap-4 text-success">
|
||||
<CheckCircle size={40}/>
|
||||
<p>
|
||||
Email confirmed<br/>
|
||||
You will be redirected shortly
|
||||
</p>
|
||||
</div>
|
||||
: validationResult === TokenValidationResult.EXPIRED ?
|
||||
<div className="flex flex-row items-center gap-4 text-warning">
|
||||
<WarningCircle size={40}/>
|
||||
<p>
|
||||
Expired token<br/>
|
||||
Please request a new one
|
||||
</p>
|
||||
</div>
|
||||
:
|
||||
<div className="flex flex-row items-center gap-4 text-danger">
|
||||
<Warning size={40}/>
|
||||
<p>
|
||||
Invalid token<br/>
|
||||
Please try again
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex flex-col min-h-svh">
|
||||
@@ -21,6 +25,13 @@ export default function MainLayout() {
|
||||
<GameyfinLogo className="h-10 fill-foreground"/>
|
||||
</NavbarBrand>
|
||||
<NavbarContent justify="end">
|
||||
{auth.state.user?.emailConfirmed === false ?
|
||||
<NavbarItem>
|
||||
<small className="text-warning">Please confirm your email</small>
|
||||
</NavbarItem>
|
||||
:
|
||||
""
|
||||
}
|
||||
<NavbarItem>
|
||||
<ProfileMenu/>
|
||||
</NavbarItem>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<EmailConfirmation>, val baseUrl: String) :
|
||||
ApplicationEvent(source)
|
||||
|
||||
class RegistrationAttemptWithExistingEmailEvent(source: Any, val existingUser: User, val baseUrl: String) :
|
||||
ApplicationEvent(source)
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ class SecurityConfig(
|
||||
http.authorizeHttpRequests { auth: AuthorizeHttpRequestsConfigurer<HttpSecurity>.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? {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<T : TokenType>(
|
||||
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())
|
||||
}
|
||||
@@ -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<T : TokenType>(
|
||||
private val type: T,
|
||||
@@ -10,6 +11,7 @@ abstract class TokenService<T : TokenType>(
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Transactional
|
||||
open fun generate(user: User): Token<T> {
|
||||
val token = Token(
|
||||
user = user,
|
||||
@@ -24,7 +26,8 @@ abstract class TokenService<T : TokenType>(
|
||||
return tokenRepository.save(token)
|
||||
}
|
||||
|
||||
fun get(secret: String, type: T): Token<T>? {
|
||||
@Transactional
|
||||
open fun get(secret: String, type: T): Token<T>? {
|
||||
val token = tokenRepository.findBySecret(secret) ?: return null
|
||||
|
||||
return if (token.type == type) {
|
||||
@@ -36,11 +39,17 @@ abstract class TokenService<T : TokenType>(
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(token: Token<T>) {
|
||||
tokenRepository.delete(token)
|
||||
@Transactional
|
||||
open fun delete(token: Token<T>) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@ class TokenTypeUserType : UserType<TokenType> {
|
||||
|
||||
override fun returnedClass(): Class<TokenType> = 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<TokenType> {
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+28
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -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>(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)
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class User(
|
||||
|
||||
var emailConfirmed: Boolean = false,
|
||||
|
||||
var enabled: Boolean = true,
|
||||
var enabled: Boolean = false,
|
||||
|
||||
@Embedded
|
||||
@Nullable
|
||||
|
||||
+2
-1
@@ -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
|
||||
+5
-3
@@ -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) {
|
||||
+2
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user