feat(auth): invite links for several people, invite by email, registration with approval or email domains
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m20s

- Invites: link /?invite=CODE, copy link, send by email, max uses with counter
- Registration modes closed | approval | domain | domain+approval | open, waiting list with approve/reject
- REGISTRATION_MODE / REGISTRATION_DOMAINS lock the admin setting; ALLOW_REGISTRATION=true is honoured

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-31 13:43:54 +01:00
1 parent db5c5cde9b
commit af77b9e809
24 files changed
+1077 -142

No files matched your search

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Brain,
Shield,
@@ -43,13 +43,37 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
const [createSampleData, setCreateSampleData] = useState(false);
// --- Login / Invite / Register State ---
// An invite link (?invite=CODE) opens the invite form with the code filled in
const [linkInviteCode] = useState(() => new URLSearchParams(window.location.search).get('invite')?.trim().toUpperCase() || '');
const [authTab, setAuthTab] = useState<'login' | 'invite' | 'register' | 'forgot' | 'reset'>(
resetToken ? 'reset' : 'login'
resetToken ? 'reset' : linkInviteCode ? 'invite' : 'login'
);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [displayName, setDisplayName] = useState('');
const [inviteCode, setInviteCode] = useState('');
const [inviteCode, setInviteCode] = useState(linkInviteCode);
// The code is in the form now; it should not stay in the address bar (history, screenshots)
useEffect(() => {
if (!linkInviteCode) return;
const url = new URL(window.location.href);
url.searchParams.delete('invite');
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
}, [linkInviteCode]);
// Hint on the sign-up form: which addresses get in, and whether an admin approves first
const registrationHint = (() => {
const domains = (authStatus.registrationDomains || []).map((d) => '@' + d.replace(/^\*\./, '')).join(', ');
switch (authStatus.registrationMode) {
case 'approval':
return 'Nach der Registrierung gibt ein Administrator dein Konto frei.';
case 'domain':
return domains ? `Registrierung mit E-Mail-Adressen von ${domains}.` : '';
case 'domain+approval':
return domains
? `Mit Adressen von ${domains} geht es sofort, andere Konten gibt ein Administrator frei.`
: 'Nach der Registrierung gibt ein Administrator dein Konto frei.';
}
return '';
})();
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -167,6 +191,13 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
try {
const code = authTab === 'invite' ? inviteCode.trim() : '';
const res = await api.register(email.trim(), password, displayName.trim(), code);
if (res.pending || !res.user) {
// Waiting for approval: back to the login form with the explanation
setNotice(res.message || 'Dein Konto wartet auf die Freigabe durch einen Administrator.');
setPassword('');
setAuthTab('login');
return;
}
onAuthenticated(res.user);
} catch (err: any) {
setError(err.message || 'Registrierung fehlgeschlagen');
@@ -687,6 +718,9 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
{(authTab === 'invite' || authTab === 'register') && (
<form onSubmit={handleRegisterOrInvite} className="space-y-3.5">
{authTab === 'register' && registrationHint && (
<p className="text-[11px] text-muted bg-canvas border border-border rounded-btn px-2.5 py-2">{registrationHint}</p>
)}
{authTab === 'invite' && (
<div>
<label className="block text-muted mb-1">Einladungscode *</label>