/** * InvitePage * * Public page for accepting invitations via email link. * URL: /invite/:token * * This page is primarily used for NEW users who receive an invitation email * and need to register first. * * Flow for NEW users (via email link): * 1. User opens email link → lands here * 2. Token is validated and stored in localStorage * 3. User clicks "Registrieren" → redirects to /register * 4. After registration, user logs in * 5. Login page checks localStorage, redirects back here * 6. User clicks "Einladung annehmen" * * For EXISTING users: * The in-app notification system handles invitations directly. * When an invitation is created for an existing user, a notification * appears in their notification bell with accept/decline buttons. */ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import { useInvitations, type InvitationValidation } from '../hooks/useInvitations'; import api from '../api'; import { getUserDataCache } from '../utils/userCache'; import { FaCheckCircle, FaTimesCircle, FaSpinner, FaSignInAlt, FaUserPlus } from 'react-icons/fa'; import styles from './InvitePage.module.css'; // Key for storing pending invitation token export const PENDING_INVITATION_KEY = 'pendingInvitationToken'; export const InvitePage: React.FC = () => { const { token } = useParams<{ token: string }>(); const navigate = useNavigate(); const { validateInvitation, acceptInvitation } = useInvitations(); // Check if user has an active session const isAuthenticated = !!sessionStorage.getItem('auth_authority'); // State const [validation, setValidation] = useState(null); const [validating, setValidating] = useState(true); const [accepting, setAccepting] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); const [userMismatch, setUserMismatch] = useState(false); const [userExists, setUserExists] = useState(null); // 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); // If invitation is valid but user is not authenticated, // store the token for later use after login/registration // Use localStorage instead of sessionStorage to persist across tabs // (e.g., when user opens password reset email in a new tab) if (result.valid && !isAuthenticated) { localStorage.setItem(PENDING_INVITATION_KEY, token); // No targetUsername = new-user invitation (email only) -> only show "Neues Konto erstellen" if (!result.targetUsername) { setUserExists(false); // Treat as new user, show only register } else { // Check if the target username already has an account try { const resp = await api.get(`/api/local/available`, { params: { username: result.targetUsername } }); // available=true means username is free -> user does NOT exist setUserExists(!resp.data.available); } catch { // On error, default to showing both options setUserExists(null); } } } if (result.valid && isAuthenticated && result.targetUsername) { const cachedUser = getUserDataCache(); if (cachedUser?.username && cachedUser.username.toLowerCase() !== result.targetUsername.toLowerCase()) { localStorage.removeItem(PENDING_INVITATION_KEY); setUserMismatch(true); } } setValidating(false); }; validate(); }, [token, validateInvitation, isAuthenticated]); // Accept invitation (for authenticated users) const handleAccept = async () => { if (!token) return; setAccepting(true); setError(null); const result = await acceptInvitation(token); if (result.success) { // Clear pending invitation token localStorage.removeItem(PENDING_INVITATION_KEY); setSuccess(true); // Redirect to dashboard after 2 seconds setTimeout(() => { navigate('/'); }, 2000); } else if (result.error?.includes('401') || result.error?.includes('Not authenticated')) { // Session expired — clear auth and redirect to login sessionStorage.removeItem('auth_authority'); handleLoginRedirect(); } else { setError(result.error || 'Fehler beim Annehmen der Einladung'); } setAccepting(false); }; // Handle redirect to login (stores token first, passes invitation data for pre-fill) const handleLoginRedirect = () => { if (token) { localStorage.setItem(PENDING_INVITATION_KEY, token); } navigate('/login', { state: { from: { pathname: `/invite/${token}` }, invitationUsername: validation?.targetUsername || '', } }); }; // Handle redirect to register (stores token first, passes invitation data for pre-fill) const handleRegisterRedirect = () => { if (token) { localStorage.setItem(PENDING_INVITATION_KEY, token); } navigate('/register', { state: { from: { pathname: `/invite/${token}` }, pendingInvitation: true, invitationUsername: validation?.targetUsername || '', invitationEmail: validation?.email || '', } }); }; // Loading state if (validating) { return (

Einladung wird überprüft...

); } // Invalid invitation if (!validation?.valid) { return (

Ungültige Einladung

{validation?.reason || 'Diese Einladung ist nicht gültig.'}

Zur Anmeldung
); } // Success state if (success) { return (

Erfolgreich!

Sie wurden erfolgreich zum Mandanten hinzugefügt.

Sie werden weitergeleitet...

); } // Authenticated but invitation is for a different user if (userMismatch && validation?.valid) { const cachedUser = getUserDataCache(); return (

Falsche Anmeldung

Diese Einladung ist für {validation.targetUsername} bestimmt. Sie sind als {cachedUser?.username || 'anderer Benutzer'} angemeldet.

Bitte melden Sie sich ab und mit dem richtigen Konto wieder an.

Zum Dashboard
); } // Already authenticated - show accept button const isFeatureInvite = !!validation.featureInstanceId; const introText = isFeatureInvite ? 'Sie wurden eingeladen, einem Mandanten und einem Feature beizutreten.' : 'Sie wurden eingeladen, einem Mandanten beizutreten.'; const rolesLabel = isFeatureInvite ? 'Features mit zugewiesenen Rollen' : 'Zugewiesene Rollen'; const rolesValue = validation.featureInstanceName && validation.roleLabels?.length ? `${validation.featureInstanceName} (${validation.roleLabels.join(', ')})` : validation.roleLabels?.join(', ') || ''; if (isAuthenticated) { return (

Einladung annehmen

{introText}

{validation.targetUsername && (
Eingeladen: {validation.targetUsername}
)} {validation.mandateName && (
Mandant: {validation.mandateName}
)}
Status: Angemeldet
{rolesValue && (
{rolesLabel}: {rolesValue}
)}
{error && (
{error}
)}
Abbrechen
); } // Not authenticated - show create account / link to existing return (

Einladung annehmen

{introText}

{validation.targetUsername && (
Eingeladen: {validation.targetUsername}
)} {validation.mandateName && (
Mandant: {validation.mandateName}
)} {rolesValue && (
{rolesLabel}: {rolesValue}
)}

{userExists === true && validation.targetUsername ? `Sie haben bereits ein Konto (${validation.targetUsername}). Melden Sie sich an oder erstellen Sie ein neues Konto.` : 'Erstellen Sie ein neues Konto mit Ihrem Benutzernamen oder verlinken Sie die Einladung mit Ihrem bestehenden Account.'}

{error && (
{error}
)}
oder

Neues Konto: E-Mail wird vorausgefüllt, Benutzername legen Sie selbst fest. Bestehendes Konto: Die Einladung wird nach der Anmeldung automatisch an Ihr Konto verknüpft.

); }; export default InvitePage;