Files
MindSift/frontend/src/features/auth/SetupAndAuthView.tsx
T
sandrodach 7f312fef14
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
feat(auth): add MINDA_DEMO support with auto-seeded demo accounts and username login
2026-10-25 10:12:13 +01:00

562 lines
22 KiB
TypeScript

import React, { useState } from 'react';
import {
Brain,
Shield,
Lock,
KeyRound,
UserPlus,
LogIn,
Server,
Share2,
Check,
AlertCircle,
} from 'lucide-react';
import { AuthStatus, User } from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api';
interface SetupAndAuthViewProps {
authStatus: AuthStatus;
onAuthenticated: (user: User) => void;
}
export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
authStatus,
onAuthenticated,
}) => {
// --- Setup Wizard State ---
const [setupName, setSetupName] = useState('');
const [setupEmail, setSetupEmail] = useState('');
const [setupPassword, setSetupPassword] = useState('');
const [instanceName, setInstanceName] = useState(authStatus.instanceName || 'MindSift');
const [sharingMode, setSharingMode] = useState<'open' | 'teams_only' | 'strict_private'>(
'teams_only'
);
const [allowRegistration, setAllowRegistration] = useState(false);
const [createSampleData, setCreateSampleData] = useState(false);
// --- Login / Invite / Register State ---
const [authTab, setAuthTab] = useState<'login' | 'invite' | 'register'>('login');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [displayName, setDisplayName] = useState('');
const [inviteCode, setInviteCode] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleCompleteSetup = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSubmitting(true);
triggerHaptic(15);
try {
const res = await api.completeSetup({
email: setupEmail.trim(),
password: setupPassword,
displayName: setupName.trim(),
instanceName: instanceName.trim() || 'MindSift',
sharingMode,
allowRegistration,
createSampleData,
});
onAuthenticated(res.user);
} catch (err: any) {
setError(err.message || 'Fehler bei der Ersteinrichtung');
} finally {
setSubmitting(false);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSubmitting(true);
triggerHaptic(15);
try {
const res = await api.login(email.trim(), password);
onAuthenticated(res.user);
} catch (err: any) {
setError(err.message || 'Ungültige Anmeldedaten');
} finally {
setSubmitting(false);
}
};
const handleRegisterOrInvite = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSubmitting(true);
triggerHaptic(15);
try {
const code = authTab === 'invite' ? inviteCode.trim() : '';
const res = await api.register(email.trim(), password, displayName.trim(), code);
onAuthenticated(res.user);
} catch (err: any) {
setError(err.message || 'Registrierung fehlgeschlagen');
} finally {
setSubmitting(false);
}
};
// ============================================================================
// FIRST-BOOT SERVER SETUP WIZARD
// ============================================================================
if (authStatus.needsSetup) {
return (
<div className="min-h-screen bg-canvas text-main flex flex-col items-center justify-center p-4 font-sans">
<div className="w-full max-w-xl panel-surface shadow-panel overflow-hidden">
{/* Top Brand Bar */}
<div className="p-4 bg-canvas border-b border-border flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 bg-accent text-on-accent rounded-btn flex items-center justify-center">
<Brain className="w-4 h-4" />
</div>
<div>
<h1 className="text-base font-bold text-main tracking-tight leading-none">
MindSift Ersteinrichtung
</h1>
<p className="text-[11px] text-muted mt-0.5">
Initiales Administrator-Konto & Server-Konfiguration
</p>
</div>
</div>
<span className="badge-accent uppercase tracking-wider">First Boot</span>
</div>
<form onSubmit={handleCompleteSetup} className="p-5 space-y-5 text-xs">
<p className="text-muted leading-relaxed">
Willkommen auf deiner neuen MindSift-Instanz. Es existiert noch kein Benutzerkonto in
der Datenbank. Lege jetzt das erste <strong className="text-main">Server-Admin-Konto</strong> an und
bestimme die Sicherheitseinstellungen.
</p>
{error && (
<div className="alert-err flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
<span>{error}</span>
</div>
)}
{/* Section 1: Admin Account */}
<div className="p-3.5 bg-canvas border border-border space-y-3">
<h2 className="text-xs font-bold text-main uppercase tracking-wider flex items-center gap-1.5">
<Shield className="w-3.5 h-3.5 text-accent" />
<span>1. Server-Administrator anlegen</span>
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-muted mb-1">Dein Anzeigename *</label>
<input
type="text"
value={setupName}
onChange={(e) => setSetupName(e.target.value)}
placeholder="z.B. Admin / Dein Name"
required
autoFocus
className="w-full h-8 px-2.5 input-field"
/>
</div>
<div>
<label className="block text-muted mb-1">E-Mail-Adresse (Login) *</label>
<input
type="email"
value={setupEmail}
onChange={(e) => setSetupEmail(e.target.value)}
placeholder="admin@deinedomain.de"
required
className="w-full h-8 px-2.5 input-field"
/>
</div>
</div>
<div>
<label className="block text-muted mb-1">Administrator-Passwort *</label>
<input
type="password"
value={setupPassword}
onChange={(e) => setSetupPassword(e.target.value)}
placeholder="Mindestens 4 Zeichen"
minLength={4}
required
className="w-full h-8 px-2.5 input-field"
/>
</div>
</div>
{/* Section 2: Server & Isolation */}
<div className="p-3.5 bg-canvas border border-border space-y-3">
<h2 className="text-xs font-bold text-main uppercase tracking-wider flex items-center gap-1.5">
<Server className="w-3.5 h-3.5 text-accent" />
<span>2. Instanz & Berechtigungen</span>
</h2>
<div>
<label className="block text-muted mb-1">Name der Instanz</label>
<input
type="text"
value={instanceName}
onChange={(e) => setInstanceName(e.target.value)}
placeholder="MindSift"
className="w-full h-8 px-2.5 input-field"
/>
</div>
<div>
<label className="block text-muted mb-1.5 flex items-center gap-1">
<Share2 className="w-3 h-3 text-accent" />
<span>Sichtbarkeit & Sharing-Isolation zwischen Benutzern</span>
</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<button
type="button"
onClick={() => setSharingMode('strict_private')}
className={`p-2.5 text-left border transition-colors ${
sharingMode === 'strict_private'
? 'bg-panel border-accent text-main'
: 'border-border text-muted hover:text-main'
}`}
>
<p className="font-bold text-xs">Strikt Privat</p>
<p className="text-[10px] text-muted mt-0.5">
Keiner sieht andere Nutzer. Komplett isoliert.
</p>
</button>
<button
type="button"
onClick={() => setSharingMode('teams_only')}
className={`p-2.5 text-left border transition-colors ${
sharingMode === 'teams_only'
? 'bg-panel border-accent text-main'
: 'border-border text-muted hover:text-main'
}`}
>
<p className="font-bold text-xs">Teams isoliert</p>
<p className="text-[10px] text-muted mt-0.5">
Nutzer sehen nur Mitglieder ihrer eigenen Teams.
</p>
</button>
<button
type="button"
onClick={() => setSharingMode('open')}
className={`p-2.5 text-left border transition-colors ${
sharingMode === 'open'
? 'bg-panel border-accent text-main'
: 'border-border text-muted hover:text-main'
}`}
>
<p className="font-bold text-xs">Alle sehen alle</p>
<p className="text-[10px] text-muted mt-0.5">
Offene Zusammenarbeit auf dem gesamten Server.
</p>
</button>
</div>
</div>
<div className="pt-2 border-t border-border space-y-2">
<label className="flex items-center justify-between cursor-pointer select-none">
<div>
<span className="font-medium text-main block">
Öffentliche Selbstregistrierung erlauben
</span>
<span className="text-[11px] text-muted">
Wenn deaktiviert, können neue Nutzer nur per Einladungscode oder durch einen
Admin angelegt werden.
</span>
</div>
<input
type="checkbox"
checked={allowRegistration}
onChange={(e) => setAllowRegistration(e.target.checked)}
className="w-4 h-4 accent-blue-600"
/>
</label>
<label className="flex items-center justify-between cursor-pointer select-none pt-1">
<div>
<span className="font-medium text-main block">
Start-Beispieldaten anlegen
</span>
<span className="text-[11px] text-muted">
Erstellt wenige Beispiel-Gedanken und ein Einführungsdokument zum Testen.
</span>
</div>
<input
type="checkbox"
checked={createSampleData}
onChange={(e) => setCreateSampleData(e.target.checked)}
className="w-4 h-4 accent-blue-600"
/>
</label>
</div>
</div>
<div className="flex items-center justify-end pt-1">
<button
type="submit"
disabled={submitting}
className="btn-primary h-9 px-5 text-xs font-semibold flex items-center gap-2 disabled:opacity-50"
>
<Check className="w-4 h-4" />
<span>
{submitting
? 'Richte Server ein…'
: 'Ersteinrichtung abschließen & Starten'}
</span>
</button>
</div>
</form>
</div>
</div>
);
}
// ============================================================================
// STANDARD LOGIN / INVITE CODE / REGISTRATION VIEW
// ============================================================================
return (
<div className="min-h-screen bg-canvas text-main flex flex-col items-center justify-center p-4 font-sans">
<div className="w-full max-w-md panel-surface shadow-panel overflow-hidden">
{/* Top Header */}
<div className="p-4 bg-canvas border-b border-border flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 bg-accent text-on-accent rounded-btn flex items-center justify-center">
<Brain className="w-4 h-4" />
</div>
<div>
<h1 className="text-sm font-bold text-main tracking-tight leading-none">
{authStatus.instanceName || 'MindSift'}
</h1>
<p className="text-[11px] text-muted mt-0.5">Der Kopf frei in drei Sekunden</p>
</div>
</div>
<Lock className="w-4 h-4 text-faint" />
</div>
{/* Navigation Tabs */}
<div className="flex border-b border-border bg-canvas text-xs">
<button
type="button"
onClick={() => {
setAuthTab('login');
setError(null);
}}
className={`flex-1 h-9 px-3 font-medium flex items-center justify-center gap-1.5 border-r border-border transition-colors ${
authTab === 'login'
? 'bg-panel text-main border-b-2 border-b-accent'
: 'text-muted hover:text-main'
}`}
>
<LogIn className="w-3.5 h-3.5" />
<span>Anmelden</span>
</button>
<button
type="button"
onClick={() => {
setAuthTab('invite');
setError(null);
}}
className={`flex-1 h-9 px-3 font-medium flex items-center justify-center gap-1.5 border-r border-border transition-colors ${
authTab === 'invite'
? 'bg-panel text-main border-b-2 border-b-accent'
: 'text-muted hover:text-main'
}`}
>
<KeyRound className="w-3.5 h-3.5" />
<span>Einladungscode</span>
</button>
{authStatus.allowRegistration && (
<button
type="button"
onClick={() => {
setAuthTab('register');
setError(null);
}}
className={`flex-1 h-9 px-3 font-medium flex items-center justify-center gap-1.5 transition-colors ${
authTab === 'register'
? 'bg-panel text-main border-b-2 border-b-accent'
: 'text-muted hover:text-main'
}`}
>
<UserPlus className="w-3.5 h-3.5" />
<span>Registrieren</span>
</button>
)}
</div>
{/* Form Body */}
<div className="p-5 text-xs">
{error && (
<div className="mb-4 alert-err flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
<span>{error}</span>
</div>
)}
{authTab === 'login' && (
<form onSubmit={handleLogin} className="space-y-3.5">
{authStatus.demoMode && (
<div className="p-2.5 rounded border border-accent/30 bg-accent/10 space-y-1.5">
<div className="flex items-center justify-between text-[11px] font-semibold text-accent">
<span>🧪 Demo-Modus aktiv</span>
<span className="text-[10px] text-muted font-normal">Klick zum Ausfüllen:</span>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => {
setEmail('admin');
setPassword('admin');
}}
className="flex-1 py-1 px-2 text-[11px] rounded bg-panel hover:bg-panel-elevated border border-border text-main font-mono transition-colors text-center"
>
admin / admin
</button>
<button
type="button"
onClick={() => {
setEmail('user');
setPassword('user');
}}
className="flex-1 py-1 px-2 text-[11px] rounded bg-panel hover:bg-panel-elevated border border-border text-main font-mono transition-colors text-center"
>
user / user
</button>
</div>
</div>
)}
<div>
<label className="block text-muted mb-1">Benutzername oder E-Mail-Adresse</label>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin oder name@beispiel.de"
required
autoFocus
className="w-full h-8 px-2.5 input-field"
/>
</div>
<div>
<label className="block text-muted mb-1">Passwort</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full h-8 px-2.5 input-field"
/>
</div>
<div className="pt-1">
<button
type="submit"
disabled={submitting}
className="w-full btn-primary h-9 text-xs font-semibold flex items-center justify-center gap-2 disabled:opacity-50"
>
<LogIn className="w-3.5 h-3.5" />
<span>{submitting ? 'Melde an…' : 'Anmelden'}</span>
</button>
</div>
{!authStatus.allowRegistration && (
<p className="text-[11px] text-faint text-center pt-2 border-t border-border">
Öffentliche Registrierung ist deaktiviert. Zugang nur mit Konto oder{' '}
<button
type="button"
onClick={() => setAuthTab('invite')}
className="text-accent hover:underline"
>
Einladungscode
</button>
.
</p>
)}
</form>
)}
{(authTab === 'invite' || authTab === 'register') && (
<form onSubmit={handleRegisterOrInvite} className="space-y-3.5">
{authTab === 'invite' && (
<div>
<label className="block text-muted mb-1">Einladungscode *</label>
<input
type="text"
value={inviteCode}
onChange={(e) => setInviteCode(e.target.value.toUpperCase())}
placeholder="MS-XXXXXXXX"
required
autoFocus
className="w-full h-8 px-2.5 input-field font-mono uppercase"
/>
</div>
)}
<div>
<label className="block text-muted mb-1">Anzeigename *</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Dein Name"
required
className="w-full h-8 px-2.5 input-field"
/>
</div>
<div>
<label className="block text-muted mb-1">E-Mail-Adresse *</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="name@beispiel.de"
required
className="w-full h-8 px-2.5 input-field"
/>
</div>
<div>
<label className="block text-muted mb-1">Passwort *</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Mindestens 4 Zeichen"
minLength={4}
required
className="w-full h-8 px-2.5 input-field"
/>
</div>
<div className="pt-1">
<button
type="submit"
disabled={submitting}
className="w-full btn-primary h-9 text-xs font-semibold flex items-center justify-center gap-2 disabled:opacity-50"
>
<UserPlus className="w-3.5 h-3.5" />
<span>
{submitting
? 'Konto wird erstellt…'
: authTab === 'invite'
? 'Einladung einlösen & Konto erstellen'
: 'Konto registrieren'}
</span>
</button>
</div>
</form>
)}
</div>
</div>
</div>
);
};