365 lines
10 KiB
TypeScript
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;
|