feat: add invite registration links with configurable usage limits

Admins can create one-time registration links (1–5× or unlimited uses)
with optional expiry (1d–14d or never). Recipients can register even
when public registration is disabled. Atomic usage counting prevents
race conditions, all endpoints are rate-limited.
This commit is contained in:
Maurice
2026-03-29 12:49:15 +02:00
parent d909aac751
commit 99514ddce1
15 changed files with 388 additions and 13 deletions

View File

@@ -274,6 +274,24 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'admin.toast.createError': 'Fehler beim Erstellen des Benutzers',
'admin.toast.fieldsRequired': 'Benutzername, E-Mail und Passwort sind erforderlich',
'admin.createUser': 'Benutzer anlegen',
'admin.invite.title': 'Einladungslinks',
'admin.invite.subtitle': 'Einmal-Links für die Registrierung erstellen',
'admin.invite.create': 'Link erstellen',
'admin.invite.createAndCopy': 'Erstellen & kopieren',
'admin.invite.empty': 'Noch keine Einladungslinks erstellt',
'admin.invite.maxUses': 'Max. Nutzungen',
'admin.invite.expiry': 'Gültig für',
'admin.invite.uses': 'genutzt',
'admin.invite.expiresAt': 'läuft ab am',
'admin.invite.createdBy': 'von',
'admin.invite.active': 'Aktiv',
'admin.invite.expired': 'Abgelaufen',
'admin.invite.usedUp': 'Aufgebraucht',
'admin.invite.copied': 'Einladungslink in Zwischenablage kopiert',
'admin.invite.copyLink': 'Link kopieren',
'admin.invite.deleted': 'Einladungslink gelöscht',
'admin.invite.createError': 'Fehler beim Erstellen des Einladungslinks',
'admin.invite.deleteError': 'Fehler beim Löschen des Einladungslinks',
'admin.tabs.settings': 'Einstellungen',
'admin.allowRegistration': 'Registrierung erlauben',
'admin.allowRegistrationHint': 'Neue Benutzer können sich selbst registrieren',

View File

@@ -274,6 +274,24 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'admin.toast.createError': 'Failed to create user',
'admin.toast.fieldsRequired': 'Username, email and password are required',
'admin.createUser': 'Create User',
'admin.invite.title': 'Invite Links',
'admin.invite.subtitle': 'Create one-time registration links',
'admin.invite.create': 'Create Link',
'admin.invite.createAndCopy': 'Create & Copy',
'admin.invite.empty': 'No invite links created yet',
'admin.invite.maxUses': 'Max. Uses',
'admin.invite.expiry': 'Expires after',
'admin.invite.uses': 'used',
'admin.invite.expiresAt': 'expires',
'admin.invite.createdBy': 'by',
'admin.invite.active': 'Active',
'admin.invite.expired': 'Expired',
'admin.invite.usedUp': 'Used up',
'admin.invite.copied': 'Invite link copied to clipboard',
'admin.invite.copyLink': 'Copy link',
'admin.invite.deleted': 'Invite link deleted',
'admin.invite.createError': 'Failed to create invite link',
'admin.invite.deleteError': 'Failed to delete invite link',
'admin.tabs.settings': 'Settings',
'admin.allowRegistration': 'Allow Registration',
'admin.allowRegistrationHint': 'New users can register themselves',

View File

@@ -272,6 +272,24 @@ const es: Record<string, string> = {
'admin.toast.createError': 'No se pudo crear el usuario',
'admin.toast.fieldsRequired': 'Usuario, correo y contraseña son obligatorios',
'admin.createUser': 'Crear usuario',
'admin.invite.title': 'Enlaces de invitación',
'admin.invite.subtitle': 'Crear enlaces de registro de un solo uso',
'admin.invite.create': 'Crear enlace',
'admin.invite.createAndCopy': 'Crear y copiar',
'admin.invite.empty': 'No se han creado enlaces de invitación',
'admin.invite.maxUses': 'Usos máx.',
'admin.invite.expiry': 'Expira después de',
'admin.invite.uses': 'usado(s)',
'admin.invite.expiresAt': 'expira el',
'admin.invite.createdBy': 'por',
'admin.invite.active': 'Activo',
'admin.invite.expired': 'Expirado',
'admin.invite.usedUp': 'Agotado',
'admin.invite.copied': 'Enlace de invitación copiado',
'admin.invite.copyLink': 'Copiar enlace',
'admin.invite.deleted': 'Enlace de invitación eliminado',
'admin.invite.createError': 'Error al crear el enlace',
'admin.invite.deleteError': 'Error al eliminar el enlace',
'admin.tabs.settings': 'Ajustes',
'admin.allowRegistration': 'Permitir el registro',
'admin.allowRegistrationHint': 'Los nuevos usuarios pueden registrarse por sí mismos',

View File

@@ -274,6 +274,24 @@ const fr: Record<string, string> = {
'admin.toast.createError': 'Échec de la création de l\'utilisateur',
'admin.toast.fieldsRequired': 'Le nom d\'utilisateur, l\'e-mail et le mot de passe sont requis',
'admin.createUser': 'Créer un utilisateur',
'admin.invite.title': 'Liens d\'invitation',
'admin.invite.subtitle': 'Créer des liens d\'inscription à usage unique',
'admin.invite.create': 'Créer un lien',
'admin.invite.createAndCopy': 'Créer et copier',
'admin.invite.empty': 'Aucun lien d\'invitation créé',
'admin.invite.maxUses': 'Utilisations max.',
'admin.invite.expiry': 'Expire après',
'admin.invite.uses': 'utilisé(s)',
'admin.invite.expiresAt': 'expire le',
'admin.invite.createdBy': 'par',
'admin.invite.active': 'Actif',
'admin.invite.expired': 'Expiré',
'admin.invite.usedUp': 'Épuisé',
'admin.invite.copied': 'Lien d\'invitation copié',
'admin.invite.copyLink': 'Copier le lien',
'admin.invite.deleted': 'Lien d\'invitation supprimé',
'admin.invite.createError': 'Erreur lors de la création du lien',
'admin.invite.deleteError': 'Erreur lors de la suppression du lien',
'admin.tabs.settings': 'Paramètres',
'admin.allowRegistration': 'Autoriser les inscriptions',
'admin.allowRegistrationHint': 'Les nouveaux utilisateurs peuvent s\'inscrire eux-mêmes',

View File

@@ -274,6 +274,24 @@ const nl: Record<string, string> = {
'admin.toast.createError': 'Gebruiker aanmaken mislukt',
'admin.toast.fieldsRequired': 'Gebruikersnaam, e-mail en wachtwoord zijn verplicht',
'admin.createUser': 'Gebruiker aanmaken',
'admin.invite.title': 'Uitnodigingslinks',
'admin.invite.subtitle': 'Eenmalige registratielinks aanmaken',
'admin.invite.create': 'Link aanmaken',
'admin.invite.createAndCopy': 'Aanmaken en kopiëren',
'admin.invite.empty': 'Nog geen uitnodigingslinks aangemaakt',
'admin.invite.maxUses': 'Max. gebruik',
'admin.invite.expiry': 'Verloopt na',
'admin.invite.uses': 'gebruikt',
'admin.invite.expiresAt': 'verloopt op',
'admin.invite.createdBy': 'door',
'admin.invite.active': 'Actief',
'admin.invite.expired': 'Verlopen',
'admin.invite.usedUp': 'Opgebruikt',
'admin.invite.copied': 'Uitnodigingslink gekopieerd',
'admin.invite.copyLink': 'Link kopiëren',
'admin.invite.deleted': 'Uitnodigingslink verwijderd',
'admin.invite.createError': 'Fout bij aanmaken van link',
'admin.invite.deleteError': 'Fout bij verwijderen van link',
'admin.tabs.settings': 'Instellingen',
'admin.allowRegistration': 'Registratie toestaan',
'admin.allowRegistrationHint': 'Nieuwe gebruikers kunnen zichzelf registreren',

View File

@@ -274,6 +274,24 @@ const ru: Record<string, string> = {
'admin.toast.createError': 'Ошибка создания пользователя',
'admin.toast.fieldsRequired': 'Имя пользователя, эл. почта и пароль обязательны',
'admin.createUser': 'Создать пользователя',
'admin.invite.title': 'Ссылки-приглашения',
'admin.invite.subtitle': 'Создание одноразовых ссылок для регистрации',
'admin.invite.create': 'Создать ссылку',
'admin.invite.createAndCopy': 'Создать и скопировать',
'admin.invite.empty': 'Ссылки-приглашения ещё не созданы',
'admin.invite.maxUses': 'Макс. использований',
'admin.invite.expiry': 'Действует',
'admin.invite.uses': 'использовано',
'admin.invite.expiresAt': 'истекает',
'admin.invite.createdBy': 'от',
'admin.invite.active': 'Активна',
'admin.invite.expired': 'Истекла',
'admin.invite.usedUp': 'Исчерпана',
'admin.invite.copied': 'Ссылка-приглашение скопирована',
'admin.invite.copyLink': 'Копировать ссылку',
'admin.invite.deleted': 'Ссылка-приглашение удалена',
'admin.invite.createError': 'Ошибка при создании ссылки',
'admin.invite.deleteError': 'Ошибка при удалении ссылки',
'admin.tabs.settings': 'Настройки',
'admin.allowRegistration': 'Разрешить регистрацию',
'admin.allowRegistrationHint': 'Новые пользователи могут регистрироваться самостоятельно',

View File

@@ -274,6 +274,24 @@ const zh: Record<string, string> = {
'admin.toast.createError': '创建用户失败',
'admin.toast.fieldsRequired': '用户名、邮箱和密码为必填项',
'admin.createUser': '创建用户',
'admin.invite.title': '邀请链接',
'admin.invite.subtitle': '创建一次性注册链接',
'admin.invite.create': '创建链接',
'admin.invite.createAndCopy': '创建并复制',
'admin.invite.empty': '尚未创建邀请链接',
'admin.invite.maxUses': '最大使用次数',
'admin.invite.expiry': '有效期',
'admin.invite.uses': '已使用',
'admin.invite.expiresAt': '过期时间',
'admin.invite.createdBy': '由',
'admin.invite.active': '有效',
'admin.invite.expired': '已过期',
'admin.invite.usedUp': '已用完',
'admin.invite.copied': '邀请链接已复制',
'admin.invite.copyLink': '复制链接',
'admin.invite.deleted': '邀请链接已删除',
'admin.invite.createError': '创建链接失败',
'admin.invite.deleteError': '删除链接失败',
'admin.tabs.settings': '设置',
'admin.allowRegistration': '允许注册',
'admin.allowRegistrationHint': '新用户可以自行注册',