Implement account status management

Add generic messages for account status change
Add message for account removal
Improve UX of role assignment
Fix a few bugs
This commit is contained in:
grimsi
2024-09-29 17:38:42 +02:00
parent a6e2965923
commit a6c6c7510b
16 changed files with 275 additions and 125 deletions
@@ -32,12 +32,12 @@ export default function AssignRolesModal({
user
}: AssignRolesModalProps) {
const [availableRoles, setAvailableRoles] = useState<Role[]>([]);
const [selectedRoles, setSelectedRoles] = useState<Selection>();
const [selectedRole, setSelectedRole] = useState<Selection>();
const [error, setError] = useState<string>();
useEffect(() => {
setSelectedRoles(rolesToSelection(user.roles!));
UserEndpoint.getAvailableRoles().then((availableRoles) => {
setSelectedRole(rolesToSelection(user.roles!));
UserEndpoint.getRolesBelow().then((availableRoles) => {
setAvailableRoles(availableRoles!.map((role) => ({id: role!.toString()})));
});
}, []);
@@ -47,15 +47,18 @@ export default function AssignRolesModal({
}
async function assignRoles() {
if (!selectedRoles) return;
if (!selectedRole) return;
let selectedRolesArray = Array.from(selectedRoles).map((role) => role.toString());
let selectedRolesArray = Array.from(selectedRole).map((role) => role.toString());
let result = await UserEndpoint.assignRoles(user.username, selectedRolesArray);
if (!result) return;
switch (result) {
case RoleAssignmentResult.SUCCESS:
window.location.reload();
break;
case RoleAssignmentResult.NO_ROLES_PROVIDED:
setError("Select at least one role");
break;
case RoleAssignmentResult.TARGET_POWER_LEVEL_TOO_HIGH:
setError("Power level of user too high");
break;
@@ -78,9 +81,10 @@ export default function AssignRolesModal({
<ModalBody className="flex flex-col gap-2">
<Select
items={availableRoles}
selectionMode="multiple"
selectedKeys={selectedRoles}
onSelectionChange={setSelectedRoles}
selectionMode="single"
disallowEmptySelection={true}
selectedKeys={selectedRole}
onSelectionChange={setSelectedRole}
placeholder="Select roles"
renderValue={(items: SelectedItems<Role>) => {
return (
@@ -106,7 +110,7 @@ export default function AssignRolesModal({
<Button variant="light" onPress={onClose}>
Cancel
</Button>
<Button color="primary" onPress={assignRoles} isDisabled={!selectedRoles}>
<Button color="primary" onPress={assignRoles} isDisabled={!selectedRole}>
Assign roles
</Button>
</ModalFooter>
@@ -2,7 +2,7 @@ import {Card, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, useDisclosu
import {DotsThreeVertical} from "@phosphor-icons/react";
import {useAuth} from "Frontend/util/auth";
import {useEffect, useState} from "react";
import {MessageEndpoint, PasswordResetEndpoint, RegistrationEndpoint} from "Frontend/generated/endpoints";
import {MessageEndpoint, PasswordResetEndpoint, UserEndpoint} from "Frontend/generated/endpoints";
import {AvatarEndpoint} from "Frontend/endpoints/endpoints";
import Avatar from "Frontend/components/general/Avatar";
import ConfirmUserDeletionModal from "Frontend/components/general/ConfirmUserDeletionModal";
@@ -28,30 +28,19 @@ export function UserManagementCard({user}: { user: UserInfoDto }) {
let keysToBeDisabled: string[] = [];
MessageEndpoint.isEnabled().then((isEnabled) => {
if (isEnabled) keysToBeDisabled.push("resetPassword");
if (!canUserBeDeleted()) keysToBeDisabled.push("delete")
if (!user.hasAvatar) keysToBeDisabled.push("removeAvatar");
setDisabledKeys(keysToBeDisabled);
});
UserEndpoint.canCurrentUserManage(user.username).then((canManage) => {
if (!canManage) keysToBeDisabled.push("assignRole", "disableUser", "delete");
setDisabledKeys(keysToBeDisabled);
});
}, []);
useEffect(() => {
setDropdownItems(getDropdownItems());
}, [userEnabled]);
function canUserBeDeleted(): Boolean {
// User should not be able to delete himself through this menu (can be done via "My profile")
if (auth.state.user?.username === user.username) return false;
// User should not be able to delete the SUPERADMIN
if (user.roles?.includes(Role.SUPERADMIN)) return false;
// Superadmins can delete anyone excluding themselves (and other superadmins if there are any)
if (auth.state.user?.roles?.includes(Role.SUPERADMIN)) return true;
// Admins should be only allowed to delete other users, not other admins
return !user.roles?.includes(Role.ADMIN);
}
async function resetPassword() {
let token = await PasswordResetEndpoint.createPasswordResetTokenForUser(user.username);
if (token === undefined) return;
@@ -62,37 +51,53 @@ export function UserManagementCard({user}: { user: UserInfoDto }) {
function getDropdownItems() {
let items = [];
if (!user.enabled) {
if (!user.managedBySso) {
if (!userEnabled) {
items.push(
{
key: "enableUser",
onPress: () => {
UserEndpoint.setUserEnabled(user.username, true).then(() => {
setUserEnabled(true);
})
},
label: "Enable user"
}
);
} else {
items.push(
{
key: "disableUser",
onPress: () => {
UserEndpoint.setUserEnabled(user.username, false).then(() => {
setUserEnabled(false);
})
},
label: "Disable user"
}
);
}
items.push(
{
key: "enableUser",
onPress: () => {
RegistrationEndpoint.confirmRegistration(user.username).then(() => {
setUserEnabled(true);
})
},
label: "Enable user"
key: "removeAvatar",
onPress: () => AvatarEndpoint.removeAvatarByName(user.username!),
label: "Remove avatar"
},
{
key: "assignRole",
onPress: roleAssignmentModal.onOpen,
label: "Assign role"
},
{
key: "resetPassword",
onPress: resetPassword,
label: "Reset password"
}
);
}
items.push(
{
key: "removeAvatar",
onPress: () => AvatarEndpoint.removeAvatarByName(user.username!),
label: "Remove avatar"
},
{
key: "assignRoles",
onPress: roleAssignmentModal.onOpen,
label: "Assign roles"
},
{
key: "resetPassword",
onPress: resetPassword,
label: "Reset password"
},
{
items.push({
key: "delete",
onPress: userDeletionConfirmationModal.onOpen,
label: "Delete user"
@@ -104,7 +109,8 @@ export function UserManagementCard({user}: { user: UserInfoDto }) {
return (
<>
<Card className={`flex flex-row justify-between p-2 ${userEnabled ? "" : "bg-warning/25"}`}>
<Card
className={`flex flex-row justify-between p-2 ${userEnabled ? "" : "bg-warning/25"} ${user.managedBySso ? "text-foreground/50" : ""}`}>
<div className="flex flex-row items-center gap-4">
<Avatar username={user.username}
name={user.username?.charAt(0)}
@@ -7,7 +7,8 @@ import {RegistrationEndpoint} from "Frontend/generated/endpoints";
import React, {useEffect, useState} from "react";
import {Warning} from "@phosphor-icons/react";
import {toast} from "sonner";
import TokenValidationResult from "Frontend/generated/de/grimsi/gameyfin/shared/token/TokenValidationResult";
import UserInvitationAcceptanceResult
from "Frontend/generated/de/grimsi/gameyfin/users/enums/UserInvitationAcceptanceResult";
export default function InvitationRegistrationView() {
const [searchParams, setSearchParams] = useSearchParams();
@@ -23,7 +24,7 @@ export default function InvitationRegistrationView() {
}
}, [searchParams]);
async function register(values: any) {
async function register(values: any, formik: any) {
let result = await RegistrationEndpoint.acceptInvitation(token, {
email: email,
username: values.username,
@@ -31,14 +32,17 @@ export default function InvitationRegistrationView() {
});
switch (result) {
case TokenValidationResult.VALID:
case UserInvitationAcceptanceResult.SUCCESS:
toast.success("Registration successful");
navigate("/", {replace: true});
break;
case TokenValidationResult.EXPIRED:
case UserInvitationAcceptanceResult.USERNAME_TAKEN:
formik.setFieldError("username", "Username is already taken");
break;
case UserInvitationAcceptanceResult.TOKEN_EXPIRED:
toast.error("Token is expired");
break;
case TokenValidationResult.INVALID:
case UserInvitationAcceptanceResult.TOKEN_INVALID:
default:
toast.error("Token is invalid");
break
@@ -1,9 +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.Invitation
import de.grimsi.gameyfin.shared.token.TokenType.PasswordReset
import de.grimsi.gameyfin.shared.token.TokenType.*
import de.grimsi.gameyfin.users.entities.User
import org.springframework.context.ApplicationEvent
@@ -12,7 +10,7 @@ class UserInvitationEvent(source: Any, val token: Token<Invitation>, val baseUrl
class UserRegistrationWaitingForApprovalEvent(source: Any, val newUser: User) : ApplicationEvent(source)
class UserRegistrationEvent(source: Any, val newUser: User, val baseUrl: String) : ApplicationEvent(source)
class AccountStatusChangedEvent(source: Any, val user: User, val baseUrl: String) : ApplicationEvent(source)
class EmailNeedsConfirmationEvent(source: Any, val token: Token<EmailConfirmation>, val baseUrl: String) :
ApplicationEvent(source)
@@ -23,6 +21,8 @@ class RegistrationAttemptWithExistingEmailEvent(source: Any, val existingUser: U
class PasswordResetRequestEvent(source: Any, val token: Token<PasswordReset>, val baseUrl: String) :
ApplicationEvent(source)
class AccountDeletedEvent(source: Any, val user: User, val baseUrl: String) : ApplicationEvent(source)
class GameRequestEvent(source: Any) : ApplicationEvent(source)
class GameRequestApprovalEvent(source: Any) : ApplicationEvent(source)
@@ -26,7 +26,8 @@ class SsoEnabledCondition : Condition {
statement.setString(1, ConfigProperties.SSO.OIDC.Enabled.key)
val resultSet = statement.executeQuery()
if (resultSet.next()) {
return resultSet.getBoolean("value")
val encryptedValue = resultSet.getString("value")
return EncryptionUtils.decrypt(encryptedValue).toBoolean()
}
}
} catch (_: Exception) {
@@ -33,9 +33,9 @@ class MessageService(
get() = applicationContext.getBeansOfType(AbstractMessageProvider::class.java).values.toList()
fun testCredentials(provider: String, credentials: Map<String, Any>): Boolean {
val notificationProvider = providers.find { it.providerKey == provider }
val messageProvider = providers.find { it.providerKey == provider }
val credentialsProperties = Properties().apply { putAll(credentials) }
return notificationProvider?.testCredentials(credentialsProperties)
return messageProvider?.testCredentials(credentialsProperties)
?: throw IllegalArgumentException("Provider '$provider' not found")
}
@@ -52,13 +52,13 @@ class MessageService(
}
/**
* Sends a test notification.
* Sends a test message.
* Recipient is always the current user to prevent misuse.
*/
fun sendTestNotification(templateKey: String, placeholders: Map<String, String>): Boolean {
if (!enabled) {
log.error { "No notification provider available, can't send test message" }
log.error { "No message provider available, can't send test message" }
return false
}
@@ -68,7 +68,7 @@ class MessageService(
val template = templateService.getMessageTemplate(templateKey)
sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders)
} catch (e: Exception) {
log.error(e) { "Failed to send test notification" }
log.error(e) { "Failed to send test message" }
return false
}
@@ -80,11 +80,11 @@ class MessageService(
fun onPasswordResetRequest(event: PasswordResetRequestEvent) {
if (!enabled) {
log.error { "No notification provider available, can't send password reset message" }
log.error { "No message provider available, can't send password reset message" }
return
}
log.info { "Sending password reset request notification" }
log.info { "Sending password reset request message" }
val token = event.token
val resetLink = event.baseUrl + "/reset-password?token=${token.secret}"
@@ -101,11 +101,11 @@ class MessageService(
fun onUserRegistrationWaitingForApproval(event: UserRegistrationWaitingForApprovalEvent) {
if (!enabled) {
log.error { "No notification provider available, can't send 'waiting for approval' message" }
log.error { "No message provider available, can't send 'waiting for approval' message" }
return
}
log.info { "Sending waiting for approval notification" }
log.info { "Sending waiting for approval message" }
val user = event.newUser
sendNotification(
@@ -117,23 +117,33 @@ class MessageService(
}
@Async
@EventListener(UserRegistrationEvent::class)
fun onUserRegistration(event: UserRegistrationEvent) {
@EventListener(AccountStatusChangedEvent::class)
fun onAccountStatusChanged(event: AccountStatusChangedEvent) {
if (!enabled) {
log.error { "No notification provider available, can't send registration message" }
log.error { "No message provider available, can't send registration message" }
return
}
log.info { "Sending registration notification" }
log.info { "Sending registration message" }
val user = event.newUser
sendNotification(
user.email,
"[Gameyfin] Welcome",
MessageTemplates.Welcome,
mapOf("username" to user.username, "baseUrl" to event.baseUrl)
)
val user = event.user
if (event.user.enabled) {
sendNotification(
user.email,
"[Gameyfin] Your account has been enabled",
MessageTemplates.AccountEnabled,
mapOf("username" to user.username, "baseUrl" to event.baseUrl)
)
} else {
sendNotification(
user.email,
"[Gameyfin] Your account has been disabled",
MessageTemplates.AccountDisabled,
mapOf("username" to user.username, "baseUrl" to event.baseUrl)
)
}
}
@Async
@@ -141,11 +151,11 @@ class MessageService(
fun onRegistrationAttemptWithExistingEmail(event: RegistrationAttemptWithExistingEmailEvent) {
if (!enabled) {
log.error { "No notification provider available, can't send 'registration attempt with existing email' message" }
log.error { "No message provider available, can't send 'registration attempt with existing email' message" }
return
}
log.info { "Sending registration attempt with existing email notification" }
log.info { "Sending registration attempt with existing email message" }
val user = event.existingUser
sendNotification(
@@ -161,11 +171,11 @@ class MessageService(
fun onEmailNeedsConfirmation(event: EmailNeedsConfirmationEvent) {
if (!enabled) {
log.error { "No notification provider available, can't send email confirmation message" }
log.error { "No message provider available, can't send email confirmation message" }
return
}
log.info { "Sending email confirmation notification" }
log.info { "Sending email confirmation message" }
val user = event.token.creator
val confirmationLink = event.baseUrl + "/confirm-email?token=${event.token.secret}"
@@ -182,11 +192,11 @@ class MessageService(
fun onUserInvitation(event: UserInvitationEvent) {
if (!enabled) {
log.error { "No notification provider available, can't send invitation message" }
log.error { "No message provider available, can't send invitation message" }
return
}
log.info { "Sending invitation notification" }
log.info { "Sending invitation message" }
val invitationLink = event.baseUrl + "/accept-invitation?token=${event.token.secret}"
sendNotification(
@@ -196,4 +206,23 @@ class MessageService(
mapOf("invitationLink" to invitationLink)
)
}
@Async
@EventListener(AccountDeletedEvent::class)
fun onAccountDeletion(event: AccountDeletedEvent) {
if (!enabled) {
log.error { "No message provider available, can't send account deletion message" }
return
}
log.info { "Sending account deletion message" }
sendNotification(
event.user.email,
"[Gameyfin] Your account has been deleted",
MessageTemplates.AccountDeleted,
mapOf("username" to event.user.username, "baseUrl" to event.baseUrl)
)
}
}
@@ -20,10 +20,24 @@ sealed class MessageTemplates(
listOf("username")
)
data object Welcome : MessageTemplates(
"welcome",
"Welcome",
"Template for the welcome message for new users",
data object AccountEnabled : MessageTemplates(
"account-enabled",
"Account Enabled",
"Template for the enabling of a users account",
listOf("username", "baseUrl")
)
data object AccountDisabled : MessageTemplates(
"account-disabled",
"Account Disabled",
"Template for the disabling of a users account",
listOf("username", "baseUrl")
)
data object AccountDeleted : MessageTemplates(
"account-deleted",
"Account Deleted",
"Template for the account deletion message",
listOf("username", "baseUrl")
)
@@ -43,6 +43,11 @@ class UserEndpoint(
userService.updateUser(username, updates)
}
@RolesAllowed(Role.Names.ADMIN)
fun setUserEnabled(username: String, enabled: Boolean) {
userService.setUserEnabled(username, enabled)
}
@PermitAll
fun deleteUser() {
val auth: Authentication = SecurityContextHolder.getContext().authentication
@@ -65,6 +70,11 @@ class UserEndpoint(
return roleService.getRolesBelowAuth(auth).map { it.roleName }
}
@RolesAllowed(Role.Names.ADMIN)
fun canCurrentUserManage(username: String): Boolean {
return userService.canManage(username)
}
@RolesAllowed(Role.Names.ADMIN)
fun assignRoles(username: String, roles: List<String>): RoleAssignmentResult {
return userService.assignRoles(username, roles)
@@ -4,10 +4,7 @@ import de.grimsi.gameyfin.config.ConfigProperties
import de.grimsi.gameyfin.config.ConfigService
import de.grimsi.gameyfin.core.Role
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.core.events.*
import de.grimsi.gameyfin.users.dto.UserInfoDto
import de.grimsi.gameyfin.users.dto.UserRegistrationDto
import de.grimsi.gameyfin.users.dto.UserUpdateDto
@@ -164,7 +161,7 @@ class UserService(
if (adminNeedsToApprove) {
eventPublisher.publishEvent(UserRegistrationWaitingForApprovalEvent(this, user))
} else {
eventPublisher.publishEvent(UserRegistrationEvent(this, user, Utils.getBaseUrl()))
eventPublisher.publishEvent(AccountStatusChangedEvent(this, user, Utils.getBaseUrl()))
}
if (!user.emailConfirmed) {
@@ -183,6 +180,10 @@ class UserService(
roles = setOf(Role.USER)
)
if (existsByUsername(user.username)) {
throw IllegalStateException("User with username '${user.username}' already exists")
}
return userRepository.save(user)
}
@@ -211,27 +212,22 @@ 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 assignRoles(username: String, roleNames: List<String>): RoleAssignmentResult {
if (roleNames.isEmpty()) {
return RoleAssignmentResult.NO_ROLES_PROVIDED
}
val currentUser = SecurityContextHolder.getContext().authentication
val targetUser = userByUsername(username)
val currentUserLevel = roleService.getHighestRoleFromAuthorities(currentUser.authorities).powerLevel
val targetUserLevel = roleService.getHighestRole(targetUser.roles).powerLevel
if (currentUserLevel <= targetUserLevel) {
if (!canManage(targetUser)) {
log.error { "User ${currentUser.name} tried to assign roles to user with higher or equal power level to their own" }
return RoleAssignmentResult.TARGET_POWER_LEVEL_TOO_HIGH
}
val newAssignedRoles = roleNames.mapNotNull { r -> Role.safeValueOf(r) }
val newAssignedRolesLevel = roleService.getHighestRole(newAssignedRoles).powerLevel
val currentUserLevel = roleService.getHighestRoleFromAuthorities(currentUser.authorities).powerLevel
if (currentUserLevel <= newAssignedRolesLevel) {
log.error { "User ${currentUser.name} tried to assign roles with higher or equal power level than their own" }
@@ -243,9 +239,29 @@ class UserService(
return RoleAssignmentResult.SUCCESS
}
fun canManage(targetUsername: String): Boolean {
val targetUser = userByUsername(targetUsername)
return canManage(targetUser)
}
fun canManage(targetUser: User): Boolean {
val currentUser = SecurityContextHolder.getContext().authentication
val currentUserLevel = roleService.getHighestRoleFromAuthorities(currentUser.authorities).powerLevel
val targetUserLevel = roleService.getHighestRole(targetUser.roles).powerLevel
return currentUserLevel > targetUserLevel
}
fun setUserEnabled(username: String, enabled: Boolean) {
val user = userByUsername(username)
user.enabled = enabled
userRepository.save(user)
eventPublisher.publishEvent(AccountStatusChangedEvent(this, user, Utils.getBaseUrl()))
}
fun deleteUser(username: String) {
val user = userByUsername(username)
userRepository.delete(user)
eventPublisher.publishEvent(AccountDeletedEvent(this, user, Utils.getBaseUrl()))
}
fun toUserInfo(user: User): UserInfoDto {
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.users.enums
enum class RoleAssignmentResult {
SUCCESS,
NO_ROLES_PROVIDED,
TARGET_POWER_LEVEL_TOO_HIGH,
ASSIGNED_ROLE_POWER_LEVEL_TOO_HIGH
}
@@ -0,0 +1,8 @@
package de.grimsi.gameyfin.users.enums
enum class UserInvitationAcceptanceResult {
SUCCESS,
TOKEN_INVALID,
TOKEN_EXPIRED,
USERNAME_TAKEN
}
@@ -1,15 +1,15 @@
package de.grimsi.gameyfin.users.registration
import de.grimsi.gameyfin.core.Utils
import de.grimsi.gameyfin.core.events.AccountStatusChangedEvent
import de.grimsi.gameyfin.core.events.UserInvitationEvent
import de.grimsi.gameyfin.core.events.UserRegistrationEvent
import de.grimsi.gameyfin.shared.token.TokenDto
import de.grimsi.gameyfin.shared.token.TokenRepository
import de.grimsi.gameyfin.shared.token.TokenService
import de.grimsi.gameyfin.shared.token.TokenType.Invitation
import de.grimsi.gameyfin.shared.token.TokenValidationResult
import de.grimsi.gameyfin.users.UserService
import de.grimsi.gameyfin.users.dto.UserRegistrationDto
import de.grimsi.gameyfin.users.enums.UserInvitationAcceptanceResult
import org.springframework.context.ApplicationEventPublisher
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
@@ -44,15 +44,19 @@ class InvitationService(
return payload[EMAIL_KEY]
}
fun acceptInvitation(secret: String, registration: UserRegistrationDto): TokenValidationResult {
val invitationToken = super.get(secret, Invitation) ?: return TokenValidationResult.INVALID
val email = invitationToken.payload[EMAIL_KEY] ?: return TokenValidationResult.INVALID
if (invitationToken.expired) return TokenValidationResult.EXPIRED
fun acceptInvitation(secret: String, registration: UserRegistrationDto): UserInvitationAcceptanceResult {
val invitationToken = super.get(secret, Invitation) ?: return UserInvitationAcceptanceResult.TOKEN_INVALID
val email = invitationToken.payload[EMAIL_KEY] ?: return UserInvitationAcceptanceResult.TOKEN_INVALID
if (invitationToken.expired) return UserInvitationAcceptanceResult.TOKEN_EXPIRED
val user = userService.registerUserFromInvitation(registration, email)
super.delete(invitationToken)
eventPublisher.publishEvent(UserRegistrationEvent(this, user, Utils.getBaseUrl()))
try {
val user = userService.registerUserFromInvitation(registration, email)
super.delete(invitationToken)
eventPublisher.publishEvent(AccountStatusChangedEvent(this, user, Utils.getBaseUrl()))
} catch (e: IllegalStateException) {
return UserInvitationAcceptanceResult.USERNAME_TAKEN
}
return TokenValidationResult.VALID
return UserInvitationAcceptanceResult.SUCCESS
}
}
@@ -4,9 +4,9 @@ import com.vaadin.flow.server.auth.AnonymousAllowed
import com.vaadin.hilla.Endpoint
import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.shared.token.TokenDto
import de.grimsi.gameyfin.shared.token.TokenValidationResult
import de.grimsi.gameyfin.users.UserService
import de.grimsi.gameyfin.users.dto.UserRegistrationDto
import de.grimsi.gameyfin.users.enums.UserInvitationAcceptanceResult
import jakarta.annotation.security.RolesAllowed
@AnonymousAllowed
@@ -29,7 +29,7 @@ class RegistrationEndpoint(
return !userService.existsByUsername(username)
}
fun acceptInvitation(token: String, registration: UserRegistrationDto): TokenValidationResult {
fun acceptInvitation(token: String, registration: UserRegistrationDto): UserInvitationAcceptanceResult {
return invitationService.acceptInvitation(token, registration)
}
@@ -37,11 +37,6 @@ class RegistrationEndpoint(
return invitationService.getAssociatedEmail(token)
}
@RolesAllowed(Role.Names.ADMIN)
fun confirmRegistration(username: String) {
userService.confirmRegistration(username)
}
@RolesAllowed(Role.Names.ADMIN)
fun createInvitation(email: String): TokenDto {
return invitationService.createInvitation(email)
@@ -0,0 +1,30 @@
<mjml>
<mj-head>
<mj-title>[Gameyfin] Welcome</mj-title>
<mj-attributes>
<mj-all font-family="Arial, sans-serif"/>
<mj-text font-size="16px"/>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section>
<mj-column>
<mj-image alt="Gameyfin Logo" title="Gameyfin Logo" width="128px" src="{logo}"/>
<mj-image alt="Gameyfin Logo" title="Gameyfin Logo" height="2px" padding-bottom="20px"
src="{gradient}"/>
<mj-text font-size="20px" font-family="helvetica">Hello there {username},
<br/>
<br/>
</mj-text>
<mj-text>your account has been permanently deleted.
<br/>
You will be missed!
<br/>
If you think this is a mistake, contact your <a href="{baseUrl}">Gameyfin</a> administrator.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
@@ -0,0 +1,28 @@
<mjml>
<mj-head>
<mj-title>[Gameyfin] Welcome</mj-title>
<mj-attributes>
<mj-all font-family="Arial, sans-serif"/>
<mj-text font-size="16px"/>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section>
<mj-column>
<mj-image alt="Gameyfin Logo" title="Gameyfin Logo" width="128px" src="{logo}"/>
<mj-image alt="Gameyfin Logo" title="Gameyfin Logo" height="2px" padding-bottom="20px"
src="{gradient}"/>
<mj-text font-size="20px" font-family="helvetica">Hello there {username},
<br/>
<br/>
</mj-text>
<mj-text>your account has been disabled.
<br/>
If you think this is a mistake, contact your <a href="{baseUrl}">Gameyfin</a> administrator.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
@@ -17,7 +17,7 @@
<br/>
<br/>
</mj-text>
<mj-text>your registration has been approved!
<mj-text>your account has been enabled!
<br/>
You can now start browsing games in <a href="{baseUrl}">Gameyfin</a>.
</mj-text>