ui-nyla/src/pages/InvitePage.tsx
2026-01-23 21:05:36 +01:00

365 lines
10 KiB
TypeScript

/**
* InvitePage
*
* Public page for accepting invitations.
* URL: /invite/:token
*
* Handles both:
* - Existing users (shows login or auto-accepts if already logged in)
* - New users (shows registration form)
*/
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useInvitations, type InvitationValidation, type RegisterAndAcceptData } from '../hooks/useInvitations';
// Note: useAuth not needed for InvitePage
import { FaCheckCircle, FaTimesCircle, FaSpinner, FaEnvelope, FaUser, FaLock } from 'react-icons/fa';
import styles from './InvitePage.module.css';
export const InvitePage: React.FC = () => {
const { token } = useParams<{ token: string }>();
const navigate = useNavigate();
const { validateInvitation, acceptInvitation, registerAndAccept } = useInvitations();
// Check if user has auth token (simplified check)
const isAuthenticated = !!sessionStorage.getItem('auth_authority');
// State
const [validation, setValidation] = useState<InvitationValidation | null>(null);
const [validating, setValidating] = useState(true);
const [accepting, setAccepting] = useState(false);
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
// Registration form state
const [formData, setFormData] = useState<RegisterAndAcceptData>({
token: token || '',
username: '',
email: '',
password: '',
firstname: '',
lastname: '',
});
const [confirmPassword, setConfirmPassword] = useState('');
// Validate token on mount
useEffect(() => {
const validate = async () => {
if (!token) {
setError('Kein Einladungs-Token angegeben');
setValidating(false);
return;
}
const result = await validateInvitation(token);
setValidation(result);
setValidating(false);
// Update form with token
setFormData(prev => ({ ...prev, token }));
};
validate();
}, [token, validateInvitation]);
// Auto-accept if already logged in
const handleAccept = async () => {
if (!token) return;
setAccepting(true);
setError(null);
const result = await acceptInvitation(token);
if (result.success) {
setSuccess(true);
// Redirect to dashboard after 2 seconds
setTimeout(() => {
navigate('/');
}, 2000);
} else {
setError(result.error || 'Fehler beim Annehmen der Einladung');
}
setAccepting(false);
};
// Handle registration form submission
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
// Validate passwords match
if (formData.password !== confirmPassword) {
setError('Die Passwörter stimmen nicht überein');
return;
}
// Validate password length
if (formData.password.length < 8) {
setError('Das Passwort muss mindestens 8 Zeichen lang sein');
return;
}
setAccepting(true);
const result = await registerAndAccept(formData);
if (result.success) {
setSuccess(true);
// Redirect to login after 3 seconds
setTimeout(() => {
navigate('/login');
}, 3000);
} else {
setError(result.error || 'Fehler bei der Registrierung');
}
setAccepting(false);
};
// Handle form field changes
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// Loading state
if (validating) {
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.loading}>
<FaSpinner className={styles.spinner} />
<p>Einladung wird überprüft...</p>
</div>
</div>
</div>
);
}
// Invalid invitation
if (!validation?.valid) {
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.errorState}>
<FaTimesCircle className={styles.errorIcon} />
<h1>Ungültige Einladung</h1>
<p>{validation?.reason || 'Diese Einladung ist nicht gültig.'}</p>
<Link to="/login" className={styles.primaryButton}>
Zur Anmeldung
</Link>
</div>
</div>
</div>
);
}
// Success state
if (success) {
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.successState}>
<FaCheckCircle className={styles.successIcon} />
<h1>Erfolgreich!</h1>
<p>
{isAuthenticated
? 'Sie wurden erfolgreich zum Mandanten hinzugefügt.'
: 'Ihr Konto wurde erstellt. Sie werden zur Anmeldeseite weitergeleitet...'}
</p>
</div>
</div>
</div>
);
}
// Already authenticated - show accept button
if (isAuthenticated) {
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.header}>
<h1>Einladung annehmen</h1>
<p>Sie wurden eingeladen, einem Mandanten beizutreten.</p>
</div>
<div className={styles.inviteInfo}>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Status:</span>
<span className={styles.infoValue}>Angemeldet</span>
</div>
{validation.roleLabels && validation.roleLabels.length > 0 && (
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Zugewiesene Rollen:</span>
<span className={styles.infoValue}>{validation.roleLabels.join(', ')}</span>
</div>
)}
</div>
{error && (
<div className={styles.errorMessage}>
<FaTimesCircle /> {error}
</div>
)}
<div className={styles.actions}>
<button
className={styles.primaryButton}
onClick={handleAccept}
disabled={accepting}
>
{accepting ? (
<>
<FaSpinner className={styles.spinner} /> Wird verarbeitet...
</>
) : (
'Einladung annehmen'
)}
</button>
<Link to="/" className={styles.secondaryButton}>
Abbrechen
</Link>
</div>
</div>
</div>
);
}
// Not authenticated - show registration form or login option
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.header}>
<h1>Einladung annehmen</h1>
<p>Erstellen Sie ein Konto, um die Einladung anzunehmen.</p>
</div>
{error && (
<div className={styles.errorMessage}>
<FaTimesCircle /> {error}
</div>
)}
<form onSubmit={handleRegister} className={styles.form}>
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label htmlFor="firstname">Vorname</label>
<input
type="text"
id="firstname"
name="firstname"
value={formData.firstname}
onChange={handleChange}
placeholder="Max"
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="lastname">Nachname</label>
<input
type="text"
id="lastname"
name="lastname"
value={formData.lastname}
onChange={handleChange}
placeholder="Mustermann"
/>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="username">
<FaUser /> Benutzername *
</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
placeholder="maxmustermann"
required
minLength={3}
maxLength={50}
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="email">
<FaEnvelope /> E-Mail *
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="max@example.com"
required
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="password">
<FaLock /> Passwort * (min. 8 Zeichen)
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="••••••••"
required
minLength={8}
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="confirmPassword">
<FaLock /> Passwort bestätigen *
</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
/>
</div>
<div className={styles.actions}>
<button
type="submit"
className={styles.primaryButton}
disabled={accepting}
>
{accepting ? (
<>
<FaSpinner className={styles.spinner} /> Wird verarbeitet...
</>
) : (
'Konto erstellen & Einladung annehmen'
)}
</button>
</div>
</form>
<div className={styles.divider}>
<span>oder</span>
</div>
<div className={styles.loginOption}>
<p>Sie haben bereits ein Konto?</p>
<Link to={`/login?redirect=/invite/${token}`} className={styles.secondaryButton}>
Anmelden und Einladung annehmen
</Link>
</div>
</div>
</div>
);
};
export default InvitePage;