Initial commit: IT Nexus Web-App
This commit is contained in:
313
frontend/src/pages/LoginPage.jsx
Normal file
313
frontend/src/pages/LoginPage.jsx
Normal file
@@ -0,0 +1,313 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user