Files
IT-Nexus/frontend/src/pages/LoginPage.jsx

314 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { toast } from 'react-toastify';
const LoginPage = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showForgotModal, setShowForgotModal] = useState(false);
const { login, loginWithToken } = useAuth();
const navigate = useNavigate();
const errRef = useRef(null);
// Handle OAuth2 callback
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
const userParam = params.get('user');
const err = params.get('error');
if (err) {
const messages = {
auth_failed: 'Microsoft-Anmeldung fehlgeschlagen.',
account_inactive: 'Ihr Konto ist deaktiviert. Bitte kontaktieren Sie den Administrator.',
azure_not_configured: 'Microsoft-Login ist nicht konfiguriert.',
token_failed: 'Token-Austausch fehlgeschlagen.',
internal_error: 'Interner Fehler beim Microsoft-Login.',
};
setError(messages[err] || 'Anmeldung fehlgeschlagen.');
window.history.replaceState({}, '', '/login');
return;
}
if (token && userParam) {
try {
const user = JSON.parse(decodeURIComponent(userParam));
loginWithToken(token, user);
window.history.replaceState({}, '', '/');
toast.success(`Willkommen, ${user.first_name || user.username}!`);
navigate(user.must_change_password ? '/change-password' : '/dashboard');
} catch {
setError('Fehler bei der Microsoft-Anmeldung.');
window.history.replaceState({}, '', '/login');
}
}
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
if (!username || !password) {
setError('Bitte Benutzername und Passwort eingeben.');
return;
}
setLoading(true);
try {
const data = await login(username, password);
navigate(data.user.must_change_password ? '/change-password' : '/dashboard');
toast.success('Erfolgreich angemeldet!');
} catch (err) {
setError(err.message || 'Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.');
} finally {
setLoading(false);
}
};
return (
<div className="login-split-root" data-theme={localStorage.getItem('nexus-theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')}>
<div className="login-split">
{/* ── LEFT: Brand ── */}
<aside className="login-brand">
<div className="login-brand-grid" />
<div className="login-brand-circuit" aria-hidden="true">
<svg viewBox="0 0 800 1000" preserveAspectRatio="xMidYMid slice">
<path className="lc-line" d="M0,180 L200,180 L240,140 L420,140 L460,180 L640,180 L680,220 L800,220"/>
<path className="lc-line" d="M0,360 L120,360 L160,400 L300,400 L340,360 L520,360 L560,400 L800,400"/>
<path className="lc-line" d="M0,640 L180,640 L220,680 L380,680 L420,720 L800,720"/>
<path className="lc-line" d="M0,860 L220,860 L260,820 L460,820 L500,860 L800,860"/>
<path className="lc-line" d="M140,0 L140,120 L180,160 L180,300"/>
<path className="lc-line" d="M540,0 L540,80 L580,120 L580,320"/>
<path className="lc-line" d="M280,1000 L280,880 L320,840 L320,720"/>
<path className="lc-line" d="M660,1000 L660,920 L620,880 L620,740"/>
<circle className="lc-node" cx="200" cy="180" r="3"/>
<circle className="lc-node" cx="460" cy="180" r="3"/>
<circle className="lc-node" cx="160" cy="400" r="3"/>
<circle className="lc-node" cx="340" cy="360" r="3"/>
<circle className="lc-node" cx="560" cy="400" r="3"/>
<circle className="lc-node" cx="220" cy="640" r="3"/>
<circle className="lc-node" cx="420" cy="720" r="3"/>
<circle className="lc-node" cx="500" cy="860" r="3"/>
<circle className="lc-pulse" cx="460" cy="180" r="3"/>
<circle className="lc-pulse p2" cx="340" cy="360" r="3"/>
<circle className="lc-pulse p3" cx="420" cy="720" r="3"/>
<circle className="lc-pulse p4" cx="580" cy="320" r="3"/>
</svg>
</div>
<div className="login-brand-head">
<img src="/cereda-logo.png" alt="Cereda Systems" />
<span className="login-brand-tag">
<span className="login-brand-tag-dot" />
System Online
</span>
</div>
<div className="login-brand-mid">
<span className="login-eyebrow">IT NEXUS</span>
<h1 className="login-brand-h1">
Eine Plattform.<br/>
<span className="login-accent">Deine ganze IT.</span>
</h1>
<p className="login-brand-lede">
Helpdesk, Monitoring, Patches, Identity und Secure Share vereint in einem Workspace für das CeredaITTeam.
</p>
<div className="login-features">
<div className="login-feat">
<div className="login-feat-ico">🎫</div>
<b>Helpdesk</b>
<span>Tickets & Wissensdatenbank</span>
</div>
<div className="login-feat">
<div className="login-feat-ico">📡</div>
<b>Monitoring</b>
<span>Geräte & Patch-Status</span>
</div>
<div className="login-feat">
<div className="login-feat-ico">🛡</div>
<b>Security</b>
<span>Defender & Reports</span>
</div>
</div>
</div>
</aside>
{/* ── RIGHT: Login Form ── */}
<section className="login-form-side">
<div className="login-form-top">
<button
className="login-theme-btn"
aria-label="Dark Mode umschalten"
onClick={() => {
const root = document.documentElement;
const cur = root.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
const next = cur === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
// also update wrapper
document.querySelector('.login-split-root')?.setAttribute('data-theme', next);
try { localStorage.setItem('nexus-theme', next); } catch {}
}}
>
<span className="ltt-ico sun">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</span>
<span className="ltt-ico moon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>
</svg>
</span>
<span className="ltt-thumb" />
</button>
</div>
<div className="login-stage">
<form className="login-inner" onSubmit={handleSubmit} noValidate>
<h1 className="login-h1">Anmelden.</h1>
<p className="login-sub">Willkommen zurück wähle deinen Weg.</p>
<button
type="button"
className="login-ms-btn"
onClick={() => { window.location.href = '/api/auth/microsoft'; }}
>
<span className="login-ms-logo" aria-hidden="true">
<span/><span/><span/><span/>
</span>
Mit Microsoft anmelden
</button>
<div className="login-divider">oder mit Benutzername</div>
<div className="login-field">
<label htmlFor="login-username">Benutzername</label>
<div className="login-input-wrap">
<input
id="login-username"
className="login-input"
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="benutzername"
autoComplete="username"
autoFocus
required
/>
</div>
</div>
<div className="login-field">
<label htmlFor="login-password">Passwort</label>
<div className="login-input-wrap">
<input
id="login-password"
className="login-input"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="••••••••••"
autoComplete="current-password"
required
/>
<button
type="button"
className="login-eye-btn"
onClick={() => setShowPassword(s => !s)}
aria-label="Passwort anzeigen"
>
{showPassword ? (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M17.94 17.94A10.94 10.94 0 0 1 12 20c-6.5 0-10-7-10-7a18.4 18.4 0 0 1 4.06-4.94"/><path d="M9.9 4.24A10 10 0 0 1 12 4c6.5 0 10 7 10 7a18.4 18.4 0 0 1-3.16 4.19"/><path d="m2 2 20 20"/>
</svg>
) : (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/>
</svg>
)}
</button>
</div>
</div>
<button
type="submit"
className="login-submit-btn"
disabled={loading}
>
{loading ? (
<><span className="login-spinner" />&nbsp;&nbsp;Wird angemeldet</>
) : (
<>
Anmelden
<svg className="lsb-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 12h14M13 6l6 6-6 6"/>
</svg>
</>
)}
</button>
<div className="login-foot-row">
<label className="login-remember">
<input type="checkbox" />
<span className="login-check-box">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<path d="m5 12 5 5L20 7"/>
</svg>
</span>
Angemeldet bleiben
</label>
<button
type="button"
className="login-forgot"
onClick={() => setShowForgotModal(true)}
>
Passwort vergessen?
</button>
</div>
{error && (
<div className="login-err on" ref={errRef}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/>
</svg>
<span>{error}</span>
</div>
)}
<div className="login-page-foot">
Developed by <b>Simon Grüßing</b> · Cereda Systems GmbH
</div>
</form>
</div>
</section>
</div>
{/* Passwort vergessen Modal */}
{showForgotModal && (
<div className="modal-overlay" onClick={() => setShowForgotModal(false)}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2>Passwort vergessen?</h2>
<button className="modal-close" onClick={() => setShowForgotModal(false)}>×</button>
</div>
<div className="modal-body">
<p style={{ marginBottom: 15 }}>Bitte wenden Sie sich an Ihren Administrator, um Ihr Passwort zurückzusetzen.</p>
<p style={{ color: '#64748b', fontSize: 14 }}>Aus Sicherheitsgründen können Passwörter nur von Administratoren zurückgesetzt werden.</p>
</div>
<div className="modal-footer">
<button className="btn btn-secondary" onClick={() => setShowForgotModal(false)}>Verstanden</button>
</div>
</div>
</div>
)}
</div>
);
};
export default LoginPage;