314 lines
16 KiB
JavaScript
314 lines
16 KiB
JavaScript
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 Cereda‑IT‑Team.
|
||
</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" /> 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;
|