frontend_nyla/src/pages/Login.tsx
2026-04-17 11:50:25 +02:00

262 lines
9.5 KiB
TypeScript

import { useNavigate, useLocation } from 'react-router-dom';
import { useState, useEffect } from 'react';
import { FaGoogle, FaMicrosoft, FaEnvelopeOpenText } from 'react-icons/fa';
import { useAuth, useMsalAuth, useGoogleAuth } from '../hooks/useAuthentication';
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { PENDING_INVITATION_KEY } from './InvitePage';
import OnboardingWizard from '../components/OnboardingWizard';
import styles from './Login.module.css';
import { LanguageSelector } from '../components/UiComponents/LanguageSelector';
import { useLanguage } from '../providers/language/LanguageContext';
function Login() {
const { t } = useLanguage();
const navigate = useNavigate();
const location = useLocation();
// Pre-fill username from invitation if provided via location.state
const invitationUsername = (location.state as any)?.invitationUsername || '';
const [username, setUsername] = useState(invitationUsername);
const [password, setPassword] = useState('');
const [usernameFocused, setUsernameFocused] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false);
const { login, error: loginError, isLoading: isLoginLoading } = useAuth();
const { loginWithMsal, error: msalError, isLoading: isMsalLoading } = useMsalAuth();
const { loginWithGoogle, error: googleError, isLoading: isGoogleLoading } = useGoogleAuth();
const [showOnboardingWizard, setShowOnboardingWizard] = useState(false);
// Check for pending invitation
const pendingInvitationToken = localStorage.getItem(PENDING_INVITATION_KEY);
const hasPendingInvitation = !!pendingInvitationToken;
const fromLocation = location.state?.from;
const from = (fromLocation?.pathname || "/") + (fromLocation?.search || "");
// Set page title and generate CSRF token
useEffect(() => {
document.title = "PowerOn AI Platform - Login";
// Generate CSRF token for new security implementation
generateAndStoreCSRFToken();
}, []);
// Check for autofilled inputs
useEffect(() => {
const checkAutofill = () => {
const usernameInput = document.querySelector('input[type="text"]') as HTMLInputElement;
const passwordInput = document.querySelector('input[type="password"]') as HTMLInputElement;
if (usernameInput && usernameInput.value) {
setUsername(usernameInput.value);
}
if (passwordInput && passwordInput.value) {
setPassword(passwordInput.value);
}
};
// Check immediately and after a short delay
checkAutofill();
const timer = setTimeout(checkAutofill, 100);
return () => clearTimeout(timer);
}, []);
// Handle redirect after successful login
const handleSuccessfulLogin = () => {
// If there's a pending invitation, redirect to accept it
if (pendingInvitationToken) {
navigate(`/invite/${pendingInvitationToken}`, { replace: true });
} else {
navigate(from, { replace: true });
}
};
const handleMsalLogin = async () => {
try {
console.log("Attempting MSAL login...");
const response = await loginWithMsal();
console.log("MSAL login successful:", response);
handleSuccessfulLogin();
} catch (error) {
console.error("MSAL login failed:", error);
}
};
const handleGoogleLogin = async () => {
try {
console.log("Attempting Google login...");
const response = await loginWithGoogle();
console.log("Google login successful:", response);
if (response?.isNewUser) {
setShowOnboardingWizard(true);
return;
}
handleSuccessfulLogin();
} catch (error) {
console.error("Google login failed:", error);
}
};
const handleCredentialLogin = async (e?: React.MouseEvent) => {
e?.preventDefault(); // Prevent default form submission
try {
console.log("Attempting login with:", username);
await login(username, password);
console.log("Login successful");
handleSuccessfulLogin();
} catch (error) {
console.error("Login failed:", error);
// Stay on login page to show error message
// The error will be displayed via the loginError state from useAuth hook
}
};
if (showOnboardingWizard) {
return (
<OnboardingWizard
onComplete={() => {
setShowOnboardingWizard(false);
handleSuccessfulLogin();
}}
onDismiss={() => {
setShowOnboardingWizard(false);
handleSuccessfulLogin();
}}
/>
);
}
return (
<div className={styles.container}>
<div className={styles.mainContent}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '-1.5rem' }}>
<LanguageSelector />
</div>
<div className={styles.logo}>
<img
src="/logos/poweron-logo.png"
alt="PowerOn"
className={styles.logoImage}
/>
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<div className={styles.loginForm}>
{/* Pending invitation notice */}
{hasPendingInvitation && (
<div className={styles.invitationNotice}>
<FaEnvelopeOpenText className={styles.invitationIcon} />
<span>{t('Sie haben eine ausstehende Einladung')}</span>
</div>
)}
{(loginError || msalError || googleError) && (
<div className={styles.error}>{loginError || msalError || googleError}</div>
)}
<div className={styles.floatingLabelInput}>
<input
type="text"
placeholder=" "
value={username}
onChange={(e) => setUsername(e.target.value)}
onFocus={() => setUsernameFocused(true)}
onBlur={() => setUsernameFocused(false)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleCredentialLogin();
}
}}
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`}
/>
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>{t('Benutzername')}</label>
</div>
<div className={styles.floatingLabelInput}>
<input
type="password"
placeholder=" "
value={password}
onChange={(e) => setPassword(e.target.value)}
onFocus={() => setPasswordFocused(true)}
onBlur={() => setPasswordFocused(false)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleCredentialLogin();
}
}}
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
/>
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>{t('Passwort')}</label>
</div>
<div className={styles.disclaimer}>
<p>
{t('Mit der Anmeldung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.')}
</p>
</div>
<button
className={`${styles.button} ${styles.loginButton}`}
onClick={handleCredentialLogin}
disabled={isLoginLoading}
>
{isLoginLoading ? t('wird geladen…') : t('Anmelden')}
</button>
<div className={styles.passwordResetLink}>
<button
className={styles.textButton}
onClick={() => navigate("/password-reset-request")}
>
{t('Passwort vergessen?')}
</button>
</div>
<div className={styles.divider}>
<span>{t('oder')}</span>
</div>
<button
className={`${styles.button} ${styles.microsoftButton}`}
onClick={handleMsalLogin}
disabled={isMsalLoading}
>
<div className={styles.buttonContent}>
<FaMicrosoft />
{isMsalLoading ? t('Anmeldung läuft…') : t('Mit Microsoft anmelden')}
</div>
</button>
<button
className={`${styles.button} ${styles.googleButton}`}
onClick={handleGoogleLogin}
disabled={isGoogleLoading}
>
<div className={styles.buttonContent}>
<FaGoogle />
{isGoogleLoading ? t('Anmeldung läuft…') : t('Mit Google anmelden')}
</div>
</button>
<div className={styles.registerLink}>
<span>{t('Du hast noch kein Konto?')}</span>
</div>
<div className={styles.ctaSection}>
<button
type="button"
className={styles.ctaPrimary}
onClick={() => navigate('/register', { state: location.state })}
>
{t('Kostenlos registrieren')}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default Login;