- Confirm deletion of user {user.username} by entering the username
- below
-
- setConfirmUsername(e.target.value)}/>
-
-
-
-
-
- >
- )}
-
-
-
+
+
+
+
+
+ {(item) => (
+
+ {item.label}
+
+ )}
+
+
+
+
+
+ >
)
}
\ No newline at end of file
diff --git a/src/main/frontend/util/utils.ts b/src/main/frontend/util/utils.ts
index edc40a4..0f271b1 100644
--- a/src/main/frontend/util/utils.ts
+++ b/src/main/frontend/util/utils.ts
@@ -1,6 +1,7 @@
import {type ClassValue, clsx} from "clsx"
import {twMerge} from "tailwind-merge"
import {getCsrfToken} from "Frontend/util/auth";
+import moment from 'moment-timezone';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -47,4 +48,37 @@ export async function fetchWithAuth(url: string, body: any = null, method = "POS
method: method,
body: body
});
+}
+
+/**
+ * Calculate the time difference between a given Instant and the current time in the user's timezone.
+ * @param {string} instantString - The Instant string returned by the backend.
+ * @param {string} timeZone - The user's timezone.
+ * @returns {string} - The time difference in a human-readable format.
+ */
+export function timeUntil(instantString: string, timeZone: string = moment.tz.guess()): string {
+ const givenDate = moment.tz(instantString, timeZone);
+ const now = moment.tz(timeZone);
+ const diffInSeconds = givenDate.diff(now, 'seconds');
+
+ const units = [
+ {name: "year", seconds: 31536000},
+ {name: "month", seconds: 2592000},
+ {name: "day", seconds: 86400},
+ {name: "hour", seconds: 3600},
+ {name: "minute", seconds: 60},
+ {name: "second", seconds: 1}
+ ];
+
+ const isPast = diffInSeconds < 0;
+ const absDiffInSeconds = Math.abs(diffInSeconds);
+
+ for (const unit of units) {
+ const value = Math.floor(absDiffInSeconds / unit.seconds);
+ if (value >= 1) {
+ return `${isPast ? '-' : ''}${value} ${unit.name}${value > 1 ? 's' : ''}`;
+ }
+ }
+
+ return "just now";
}
\ No newline at end of file
diff --git a/src/main/frontend/views/LoginView.tsx b/src/main/frontend/views/LoginView.tsx
index 5f87f0c..4258988 100644
--- a/src/main/frontend/views/LoginView.tsx
+++ b/src/main/frontend/views/LoginView.tsx
@@ -1,41 +1,25 @@
import {useAuth} from "Frontend/util/auth";
import {useEffect, useState} from "react";
-import {WarningCircle, XCircle} from "@phosphor-icons/react";
-import {
- Button,
- Card,
- CardBody,
- CardHeader,
- Input,
- Link,
- Modal,
- ModalBody,
- ModalContent,
- ModalFooter,
- ModalHeader,
- useDisclosure
-} from "@nextui-org/react";
-import {Alert, AlertDescription, AlertTitle} from "Frontend/@/components/ui/alert";
+import {Button, Card, CardBody, CardHeader, Link, useDisclosure} from "@nextui-org/react";
import {useNavigate} from "react-router-dom";
-import {MessageEndpoint, PasswordResetEndpoint} from "Frontend/generated/endpoints";
-import {toast} from "sonner";
+import {Form, Formik} from "formik";
+import Input from "Frontend/components/general/Input";
+import PasswordResetModal from "Frontend/components/general/PasswordResetModal";
+import SignUpModal from "Frontend/components/general/SignUpModal";
+import {RegistrationEndpoint} from "Frontend/generated/endpoints";
export default function LoginView() {
const {state, login} = useAuth();
- const {isOpen, onOpen, onOpenChange} = useDisclosure();
-
- const [hasError, setError] = useState(false);
- const [loading, setLoading] = useState(false);
- const [username, setUsername] = useState();
- const [password, setPassword] = useState();
- const [canResetPassword, setCanResetPassword] = useState(false);
- const [url, setUrl] = useState();
- const [resetEmail, setResetEmail] = useState();
-
const navigate = useNavigate();
+ const passwordResetModal = useDisclosure();
+ const signUpModal = useDisclosure();
+
+ const [url, setUrl] = useState();
+ const [signUpAllowed, setSignUpAllowed] = useState(false);
+
useEffect(() => {
- MessageEndpoint.isEnabled().then(setCanResetPassword);
+ RegistrationEndpoint.isSelfRegistrationAllowed().then(setSignUpAllowed);
}, []);
useEffect(() => {
@@ -45,9 +29,14 @@ export default function LoginView() {
}
}, [state.user]);
- async function resetPassword() {
- await PasswordResetEndpoint.requestPasswordReset(resetEmail);
- toast.success("If the email address is registered, you will receive a message with further instructions.");
+ async function tryLogin(values: any, formik: any) {
+ const {defaultUrl, error, redirectUrl} = await login(values.username, values.password);
+ if (!error) {
+ setUrl(redirectUrl ?? defaultUrl ?? '/');
+ } else {
+ formik.setFieldError("username", " "); // Mark the field red, but don't show an error message
+ formik.setFieldError("password", "Invalid username and/or password.");
+ }
}
return (
@@ -61,109 +50,47 @@ export default function LoginView() {
/>
- {hasError &&
-
-
- Error
- Wrong username and/or password
-
- }
-
+
+ {(formik: { isSubmitting: any; }) => (
+
+ )}
+
-
-
- {(onClose) => (
- <>
- Request a password reset
-
- {canResetPassword ?
- {
- setResetEmail(event.target.value);
- }}
- type="email"
- placeholder="Email"
- /> :
-
-
-
- Password self-service is disabled.
- To reset your password please contact your administrator.
-
-
- }
-
-
-
-
-
- >
- )}
-
-
+
+
);
}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt b/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt
index 06b20fb..f26f8f8 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt
@@ -57,11 +57,11 @@ sealed class ConfigProperties(
false
)
- data object Confirm : ConfigProperties(
+ data object ConfirmationRequired : ConfigProperties(
Boolean::class,
- "users.sign-ups.confirm",
+ "users.sign-ups.confirmation-required",
"Admins need to confirm new users",
- false
+ true
)
}
}
diff --git a/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt b/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt
index 08b89e4..fa9e9f0 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt
@@ -15,12 +15,11 @@ class Utils {
val scheme = request.scheme
val serverName = request.serverName
val serverPort = request.serverPort
- val contextPath = request.contextPath
return if (serverPort == 80 || serverPort == 443) {
- "$scheme://$serverName$contextPath"
+ "$scheme://$serverName"
} else {
- "$scheme://$serverName:$serverPort$contextPath"
+ "$scheme://$serverName:$serverPort"
}
}
}
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 f9bfca6..40e90fb 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/core/events/Events.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/core/events/Events.kt
@@ -2,11 +2,17 @@ package de.grimsi.gameyfin.core.events
import de.grimsi.gameyfin.shared.token.Token
import de.grimsi.gameyfin.shared.token.TokenType.PasswordReset
+import de.grimsi.gameyfin.users.entities.User
import org.springframework.context.ApplicationEvent
class UserInvitationEvent(source: Any) : ApplicationEvent(source)
-class UserRegistrationEvent(source: Any) : ApplicationEvent(source)
+class UserRegistrationWaitingForApprovalEvent(source: Any, val newUser: User) : ApplicationEvent(source)
+
+class UserRegistrationEvent(source: Any, val newUser: User, val baseUrl: String) : ApplicationEvent(source)
+
+class RegistrationAttemptWithExistingEmailEvent(source: Any, val existingUser: User, val baseUrl: String) :
+ ApplicationEvent(source)
class PasswordResetRequestEvent(source: Any, val token: Token, val baseUrl: String) :
ApplicationEvent(source)
diff --git a/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt b/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt
index f3c7b3f..2cc65b7 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt
@@ -1,6 +1,9 @@
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.messages.providers.AbstractMessageProvider
import de.grimsi.gameyfin.messages.templates.MessageTemplateService
import de.grimsi.gameyfin.messages.templates.MessageTemplates
@@ -95,4 +98,64 @@ class MessageService(
mapOf("username" to token.user.username, "resetLink" to resetLink)
)
}
+
+ @Async
+ @EventListener(UserRegistrationWaitingForApprovalEvent::class)
+ fun onUserRegistrationWaitingForApproval(event: UserRegistrationWaitingForApprovalEvent) {
+
+ if (!enabled) {
+ log.error { "No notification provider available, can't send 'waiting for approval' message" }
+ return
+ }
+
+ log.info { "Sending waiting for approval notification" }
+
+ val user = event.newUser
+ sendNotification(
+ user.email,
+ "[Gameyfin] Waiting for Approval",
+ MessageTemplates.WaitingForApproval,
+ mapOf("username" to user.username)
+ )
+ }
+
+ @Async
+ @EventListener(UserRegistrationEvent::class)
+ fun onUserRegistration(event: UserRegistrationEvent) {
+
+ if (!enabled) {
+ log.error { "No notification provider available, can't send registration message" }
+ return
+ }
+
+ log.info { "Sending registration notification" }
+
+ val user = event.newUser
+ sendNotification(
+ user.email,
+ "[Gameyfin] Welcome",
+ MessageTemplates.Welcome,
+ mapOf("username" to user.username, "baseUrl" to event.baseUrl)
+ )
+ }
+
+ @Async
+ @EventListener(RegistrationAttemptWithExistingEmailEvent::class)
+ fun onRegistrationAttemptWithExistingEmail(event: RegistrationAttemptWithExistingEmailEvent) {
+
+ if (!enabled) {
+ log.error { "No notification provider available, can't send 'registration attempt with existing email' message" }
+ return
+ }
+
+ log.info { "Sending registration attempt with existing email notification" }
+
+ val user = event.existingUser
+ sendNotification(
+ user.email,
+ "[Gameyfin] Account alert",
+ MessageTemplates.RegistrationAttemptWithExistingEmail,
+ mapOf("username" to user.username, "passwordResetLink" to event.baseUrl)
+ )
+ }
}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/messages/templates/MessageTemplates.kt b/src/main/kotlin/de/grimsi/gameyfin/messages/templates/MessageTemplates.kt
index 2e0e5a3..7db7ba6 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/messages/templates/MessageTemplates.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/messages/templates/MessageTemplates.kt
@@ -13,11 +13,25 @@ sealed class MessageTemplates(
listOf("invitationLink")
)
+ data object WaitingForApproval : MessageTemplates(
+ "waiting-for-approval",
+ "Waiting for approval",
+ "Template for the waiting for approval message for new users",
+ listOf("username")
+ )
+
data object Welcome : MessageTemplates(
"welcome",
"Welcome",
"Template for the welcome message for new users",
- listOf("username")
+ listOf("username", "baseUrl")
+ )
+
+ data object RegistrationAttemptWithExistingEmail : MessageTemplates(
+ "email-already-registered",
+ "Someone tried to register with your email",
+ "Template for the email already registered message",
+ listOf("username", "passwordResetLink")
)
data object EmailConfirmation : MessageTemplates(
diff --git a/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenDto.kt b/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenDto.kt
new file mode 100644
index 0000000..04641a8
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/shared/token/TokenDto.kt
@@ -0,0 +1,16 @@
+package de.grimsi.gameyfin.shared.token
+
+import java.time.Instant
+import kotlin.time.toJavaDuration
+
+data class TokenDto(
+ val secret: String,
+ val type: String,
+ val expiresAt: Instant
+) {
+ constructor(token: Token<*>) : this(
+ secret = token.secret,
+ type = token.type.key,
+ expiresAt = token.createdOn?.plus(token.type.expiration.toJavaDuration()) ?: Instant.MIN
+ )
+}
\ No newline at end of file
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetEndpoint.kt
index 902ba93..31cfab1 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetEndpoint.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetEndpoint.kt
@@ -3,6 +3,7 @@ package de.grimsi.gameyfin.users
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 jakarta.annotation.security.RolesAllowed
@@ -20,8 +21,8 @@ class PasswordResetEndpoint(
}
@RolesAllowed(Roles.Names.ADMIN)
- fun createPasswordResetTokenForUser(username: String): String {
- return passwordResetService.generate(username).secret
+ fun createPasswordResetTokenForUser(username: String): TokenDto {
+ return passwordResetService.generate(username)
}
fun resetPassword(secret: String, newPassword: String): TokenValidationResult {
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetService.kt b/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetService.kt
index b981ae9..e85eba4 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetService.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/PasswordResetService.kt
@@ -3,11 +3,8 @@ package de.grimsi.gameyfin.users
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.Token
-import de.grimsi.gameyfin.shared.token.TokenRepository
-import de.grimsi.gameyfin.shared.token.TokenService
+import de.grimsi.gameyfin.shared.token.*
import de.grimsi.gameyfin.shared.token.TokenType.PasswordReset
-import de.grimsi.gameyfin.shared.token.TokenValidationResult
import de.grimsi.gameyfin.users.entities.User
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.context.ApplicationEventPublisher
@@ -27,9 +24,6 @@ class PasswordResetService(
private val secureRandom = SecureRandom()
- private val baseUrl: String
- get() = Utils.getBaseUrl()
-
override fun generate(user: User): Token {
if (user.oidcProviderId != null) {
throw IllegalStateException("Cannot create password reset token for user '${user.username}' because user is managed externally")
@@ -44,7 +38,7 @@ class PasswordResetService(
* - The user has no confirmed email address
* - The user is not managed externally
*/
- fun generate(username: String): Token {
+ fun generate(username: String): TokenDto {
if (messageService.enabled) {
throw IllegalStateException("Cannot create password reset token for user '$username' because self-service is enabled")
}
@@ -52,11 +46,12 @@ class PasswordResetService(
val user = userService.getByUsername(username)
?: throw IllegalArgumentException("Cannot create password reset token for user '$username' because user does not exist")
- if (user.emailConfirmed == true) {
+ if (user.emailConfirmed) {
throw IllegalStateException("Cannot create password reset token for user '$username' because self-service is enabled")
}
- return generate(user)
+ val token = generate(user)
+ return TokenDto(token)
}
/**
@@ -88,7 +83,7 @@ class PasswordResetService(
}
val token = generate(user)
- eventPublisher.publishEvent(PasswordResetRequestEvent(this, token, baseUrl))
+ eventPublisher.publishEvent(PasswordResetRequestEvent(this, token, Utils.getBaseUrl()))
// Simulate a delay to prevent timing attacks
Thread.sleep(secureRandom.nextLong(1024))
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/RegistrationEndpoint.kt b/src/main/kotlin/de/grimsi/gameyfin/users/RegistrationEndpoint.kt
new file mode 100644
index 0000000..786f2bb
--- /dev/null
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/RegistrationEndpoint.kt
@@ -0,0 +1,32 @@
+package de.grimsi.gameyfin.users
+
+import com.vaadin.flow.server.auth.AnonymousAllowed
+import com.vaadin.hilla.Endpoint
+import de.grimsi.gameyfin.core.Roles
+import de.grimsi.gameyfin.users.dto.UserRegistrationDto
+import jakarta.annotation.security.RolesAllowed
+
+@AnonymousAllowed
+@Endpoint
+class RegistrationEndpoint(
+ private val userService: UserService
+) {
+ fun isSelfRegistrationAllowed(): Boolean {
+ return userService.selfRegistrationAllowed
+ }
+
+ fun registerUser(registration: UserRegistrationDto) {
+ userService.selfRegisterUser(registration)
+
+ // No return value to prevent enumeration attacks
+ }
+
+ fun isUsernameAvailable(username: String): Boolean {
+ return !userService.existsByUsername(username)
+ }
+
+ @RolesAllowed(Roles.Names.ADMIN)
+ fun confirmRegistration(username: String) {
+ userService.confirmRegistration(username)
+ }
+}
\ 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 79edfc7..9862e61 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/UserEndpoint.kt
@@ -3,9 +3,7 @@ package de.grimsi.gameyfin.users
import com.vaadin.hilla.Endpoint
import de.grimsi.gameyfin.core.Roles
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.entities.User
import jakarta.annotation.security.PermitAll
import jakarta.annotation.security.RolesAllowed
import org.springframework.security.core.Authentication
@@ -16,7 +14,6 @@ import org.springframework.security.core.context.SecurityContextHolder
class UserEndpoint(
private val userService: UserService
) {
-
@PermitAll
fun getUserInfo(): UserInfoDto {
val auth: Authentication = SecurityContextHolder.getContext().authentication
@@ -28,12 +25,6 @@ class UserEndpoint(
return userService.getAllUsers()
}
- @PermitAll
- fun registerUser(registration: UserRegistrationDto): UserInfoDto {
- val user: User = registerUser(registration, listOf(Roles.USER))
- return userService.toUserInfo(user)
- }
-
@PermitAll
fun updateUser(updates: UserUpdateDto) {
val auth: Authentication = SecurityContextHolder.getContext().authentication
@@ -55,14 +46,4 @@ class UserEndpoint(
fun deleteUserByName(username: String) {
userService.deleteUser(username)
}
-
- private fun registerUser(registration: UserRegistrationDto, roles: List): User {
- val user = User(
- username = registration.username,
- password = registration.password,
- email = registration.email
- )
-
- 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 041130b..61540ad 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt
@@ -1,7 +1,14 @@
package de.grimsi.gameyfin.users
+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.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.entities.Avatar
import de.grimsi.gameyfin.users.entities.Role
@@ -10,6 +17,7 @@ import de.grimsi.gameyfin.users.persistence.AvatarContentStore
import de.grimsi.gameyfin.users.persistence.UserRepository
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.transaction.Transactional
+import org.springframework.context.ApplicationEventPublisher
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
@@ -30,11 +38,16 @@ class UserService(
private val avatarStore: AvatarContentStore,
private val passwordEncoder: PasswordEncoder,
private val roleService: RoleService,
- private val sessionService: SessionService
+ private val sessionService: SessionService,
+ private val config: ConfigService,
+ private val eventPublisher: ApplicationEventPublisher
) : UserDetailsService {
private val log = KotlinLogging.logger {}
+ val selfRegistrationAllowed: Boolean
+ get() = config.get(ConfigProperties.Users.SignUps.Allow) == true
+
override fun loadUserByUsername(username: String): UserDetails {
val user = userByUsername(username)
@@ -124,6 +137,39 @@ class UserService(
return userRepository.save(user)
}
+ fun selfRegisterUser(registration: UserRegistrationDto) {
+ if (!selfRegistrationAllowed) {
+ throw IllegalStateException("Sign ups are not allowed")
+ }
+
+ if (existsByUsername(registration.username)) {
+ throw IllegalStateException("User with username '${registration.username}' already exists")
+ }
+
+ userRepository.findByEmail(registration.email)?.let {
+ eventPublisher.publishEvent(RegistrationAttemptWithExistingEmailEvent(this, it, Utils.getBaseUrl()))
+ return
+ }
+
+ val adminNeedsToApprove = config.get(ConfigProperties.Users.SignUps.ConfirmationRequired) == true
+
+ var user = User(
+ username = registration.username,
+ password = passwordEncoder.encode(registration.password),
+ email = registration.email,
+ enabled = !adminNeedsToApprove,
+ roles = roleService.toRoles(listOf(Roles.USER))
+ )
+
+ user = userRepository.save(user)
+
+ if (adminNeedsToApprove) {
+ eventPublisher.publishEvent(UserRegistrationWaitingForApprovalEvent(this, user))
+ } else {
+ eventPublisher.publishEvent(UserRegistrationEvent(this, user, Utils.getBaseUrl()))
+ }
+ }
+
fun updateUser(username: String, updates: UserUpdateDto) {
val user = userByUsername(username)
@@ -147,6 +193,13 @@ class UserService(
userRepository.save(user)
}
+ fun confirmRegistration(username: String) {
+ val user = userByUsername(username)
+ user.enabled = true
+ userRepository.save(user)
+ eventPublisher.publishEvent(UserRegistrationEvent(this, user, Utils.getBaseUrl()))
+ }
+
fun deleteUser(username: String) {
val user = userByUsername(username)
userRepository.delete(user)
@@ -157,6 +210,8 @@ class UserService(
username = user.username,
email = user.email,
emailConfirmed = user.emailConfirmed,
+ isEnabled = user.enabled,
+ hasAvatar = user.avatar != null,
managedBySso = user.oidcProviderId != null,
roles = user.roles.map { r -> r.rolename }
)
diff --git a/src/main/kotlin/de/grimsi/gameyfin/users/dto/UserInfoDto.kt b/src/main/kotlin/de/grimsi/gameyfin/users/dto/UserInfoDto.kt
index 50aa358..590133f 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/users/dto/UserInfoDto.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/dto/UserInfoDto.kt
@@ -5,5 +5,7 @@ data class UserInfoDto(
val managedBySso: Boolean,
val email: String,
val emailConfirmed: Boolean,
+ val isEnabled: Boolean,
+ val hasAvatar: Boolean,
var roles: List
)
\ 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 c41f235..c8d538a 100644
--- a/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt
+++ b/src/main/kotlin/de/grimsi/gameyfin/users/entities/User.kt
@@ -27,8 +27,7 @@ class User(
@Convert(converter = EncryptionConverter::class)
var email: String,
- // TODO: Add email confirmation
- var emailConfirmed: Boolean = true,
+ var emailConfirmed: Boolean = false,
var enabled: Boolean = true,
diff --git a/src/main/resources/templates/messages/email-already-registered.mjml b/src/main/resources/templates/messages/email-already-registered.mjml
new file mode 100644
index 0000000..0ef4d97
--- /dev/null
+++ b/src/main/resources/templates/messages/email-already-registered.mjml
@@ -0,0 +1,34 @@
+
+
+ [Gameyfin] Someone tried to register with your email
+
+
+
+
+
+
+
+
+
+
+
+ Hello there {username},
+
+
+
+ someone just tried to register a new account with this email address.
+
+
+ If you just forgot your password, you can reset it by visiting
+ Gameyfin
+ and
+ clicking on the "Forgot password?" button.
+
+
+ If this wasn't you, please ignore this email.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/messages/waiting-for-approval.mjml b/src/main/resources/templates/messages/waiting-for-approval.mjml
new file mode 100644
index 0000000..53d8532
--- /dev/null
+++ b/src/main/resources/templates/messages/waiting-for-approval.mjml
@@ -0,0 +1,28 @@
+
+
+ [Gameyfin] Waiting for Approval
+
+
+
+
+
+
+
+
+
+
+
+ Hello there {username},
+
+
+
+ your registration was successful!
+
+ Before you can log in, you need to wait for an admin to approve your account.
+ We will notify you as soon as your account is approved.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/messages/welcome.mjml b/src/main/resources/templates/messages/welcome.mjml
index ef7c93c..911677e 100644
--- a/src/main/resources/templates/messages/welcome.mjml
+++ b/src/main/resources/templates/messages/welcome.mjml
@@ -16,9 +16,9 @@
- your registration was successful!
+ your registration has been approved!
- You can now start browsing games in Gameyfin.
+ You can now start browsing games in Gameyfin.