262 lines
9.5 KiB
TypeScript
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;
|