Auth: Magic Link Login + Globe-Zugangssteuerung
- Magic Link Login (E-Mail + 6-stelliger Code) - JWT-basierte Session (24h) - Prueft: is_active=1 UND globe_access=1 - Akzeptiert auch Monitor-JWT-Tokens (Kompatibilitaet) - Globe-spezifisches E-Mail-Template (Dark Theme) - Alle Daten-APIs hinter Auth-Middleware - Login-Seite mit taktischem Design - Auto-Redirect bei fehlendem/abgelaufenem Token - Fetch-Wrapper injiziert Authorization Header automatisch
Dieser Commit ist enthalten in:
@@ -9,6 +9,23 @@
|
||||
<link rel="stylesheet" href="/static/css/globe.css">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
// Auth-Check: Ohne Token zum Login
|
||||
if (!localStorage.getItem('globe_token')) { window.location.href = '/login'; }
|
||||
// Auth-Header fuer alle Fetch-Calls
|
||||
var _origFetch = window.fetch;
|
||||
window.fetch = function(url, opts) {
|
||||
opts = opts || {};
|
||||
if (typeof url === 'string' && url.startsWith('/api/') && !url.includes('/auth/')) {
|
||||
opts.headers = opts.headers || {};
|
||||
opts.headers['Authorization'] = 'Bearer ' + localStorage.getItem('globe_token');
|
||||
}
|
||||
return _origFetch(url, opts).then(function(r) {
|
||||
if (r.status === 401 || r.status === 403) { localStorage.removeItem('globe_token'); window.location.href = '/login'; }
|
||||
return r;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<!-- Header -->
|
||||
<header id="header">
|
||||
<div class="header-brand">
|
||||
@@ -30,13 +47,13 @@
|
||||
<h3 class="panel-title">LAYER</h3>
|
||||
<div class="panel-section">
|
||||
<label class="layer-toggle">
|
||||
<input type="checkbox" id="layer-flights">
|
||||
<input type="checkbox" id="layer-flights" checked>
|
||||
<span class="layer-dot dot-flights"></span>
|
||||
<span class="layer-name">Flugverkehr</span>
|
||||
<span class="layer-count" id="count-flights">-</span>
|
||||
</label>
|
||||
<label class="layer-toggle">
|
||||
<input type="checkbox" id="layer-ships">
|
||||
<input type="checkbox" id="layer-ships" checked>
|
||||
<span class="layer-dot dot-ships"></span>
|
||||
<span class="layer-name">Schiffsverkehr</span>
|
||||
<span class="layer-count" id="count-ships">-</span>
|
||||
@@ -57,7 +74,7 @@
|
||||
<div class="panel-divider"></div>
|
||||
<div class="panel-section">
|
||||
<label class="layer-toggle">
|
||||
<input type="checkbox" id="layer-daynight">
|
||||
<input type="checkbox" id="layer-daynight" checked>
|
||||
<span class="layer-dot dot-daynight"></span>
|
||||
<span class="layer-name">Tag/Nacht</span>
|
||||
</label>
|
||||
|
||||
152
static/login.html
Normale Datei
152
static/login.html
Normale Datei
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AegisSight Globe — Login</title>
|
||||
<style>
|
||||
:root { --bg: #0b1121; --accent: #00ff88; --border: rgba(0,255,136,0.15); --text: #e8eaf0; --text-dim: rgba(255,255,255,0.5); --mono: 'JetBrains Mono','Courier New',monospace; }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { min-height:100vh; background:var(--bg); color:var(--text); font-family:var(--mono); display:flex; align-items:center; justify-content:center; }
|
||||
.login-box { width:380px; background:rgba(11,17,33,0.95); border:1px solid var(--border); border-radius:12px; padding:40px 32px; box-shadow:0 16px 64px rgba(0,0,0,0.5); }
|
||||
.logo { text-align:center; margin-bottom:32px; }
|
||||
.logo svg { width:40px; height:40px; }
|
||||
.logo h1 { font-size:14px; letter-spacing:3px; color:var(--accent); margin-top:12px; }
|
||||
.logo p { font-size:11px; color:var(--text-dim); margin-top:4px; }
|
||||
.form-group { margin-bottom:20px; }
|
||||
label { display:block; font-size:10px; letter-spacing:1.5px; color:var(--text-dim); margin-bottom:6px; text-transform:uppercase; }
|
||||
input[type="email"], input[type="text"] {
|
||||
width:100%; padding:12px 14px; background:rgba(255,255,255,0.05); border:1px solid var(--border);
|
||||
border-radius:6px; color:var(--text); font-family:var(--mono); font-size:14px; outline:none; transition:border-color 0.2s;
|
||||
}
|
||||
input:focus { border-color:var(--accent); }
|
||||
.code-input { text-align:center; letter-spacing:8px; font-size:24px; font-weight:700; }
|
||||
.btn {
|
||||
width:100%; padding:12px; background:rgba(0,255,136,0.1); border:1px solid var(--accent);
|
||||
border-radius:6px; color:var(--accent); font-family:var(--mono); font-size:13px; font-weight:700;
|
||||
letter-spacing:1px; cursor:pointer; transition:all 0.2s;
|
||||
}
|
||||
.btn:hover { background:rgba(0,255,136,0.2); }
|
||||
.btn:disabled { opacity:0.4; cursor:not-allowed; }
|
||||
.error { color:#ff4444; font-size:12px; margin-top:8px; display:none; }
|
||||
.success { color:var(--accent); font-size:12px; margin-top:8px; display:none; }
|
||||
.step { display:none; }
|
||||
.step.active { display:block; }
|
||||
.back-link { display:block; text-align:center; margin-top:16px; font-size:11px; color:var(--text-dim); cursor:pointer; }
|
||||
.back-link:hover { color:var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#00ff88" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/>
|
||||
</svg>
|
||||
<h1>AEGISSIGHT GLOBE</h1>
|
||||
<p>Geospatial Intelligence Dashboard</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: E-Mail -->
|
||||
<div id="step-email" class="step active">
|
||||
<div class="form-group">
|
||||
<label>E-Mail-Adresse</label>
|
||||
<input type="email" id="input-email" placeholder="name@beispiel.de" autofocus>
|
||||
</div>
|
||||
<button class="btn" id="btn-send" onclick="requestLink()">Zugangscode anfordern</button>
|
||||
<div class="error" id="error-email"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Code -->
|
||||
<div id="step-code" class="step">
|
||||
<div class="form-group">
|
||||
<label>6-stelliger Zugangscode</label>
|
||||
<input type="text" id="input-code" class="code-input" maxlength="6" placeholder="------" inputmode="numeric" pattern="[0-9]*">
|
||||
</div>
|
||||
<div class="success" id="success-code" style="display:block;margin-bottom:16px;">Zugangscode wurde per E-Mail gesendet.</div>
|
||||
<button class="btn" id="btn-verify" onclick="verifyCode()">Verifizieren</button>
|
||||
<div class="error" id="error-code"></div>
|
||||
<span class="back-link" onclick="showStep('email')">Andere E-Mail verwenden</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check if already logged in
|
||||
if (localStorage.getItem('globe_token')) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
function showStep(step) {
|
||||
document.querySelectorAll('.step').forEach(function(el) { el.classList.remove('active'); });
|
||||
document.getElementById('step-' + step).classList.add('active');
|
||||
document.querySelectorAll('.error').forEach(function(el) { el.style.display = 'none'; });
|
||||
}
|
||||
|
||||
function showError(id, msg) {
|
||||
var el = document.getElementById(id);
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
}
|
||||
|
||||
async function requestLink() {
|
||||
var email = document.getElementById('input-email').value.trim();
|
||||
if (!email) return;
|
||||
var btn = document.getElementById('btn-send');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Sende...';
|
||||
document.getElementById('error-email').style.display = 'none';
|
||||
|
||||
try {
|
||||
var resp = await fetch('/api/auth/request-link', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email }),
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
showError('error-email', data.detail || 'Fehler');
|
||||
} else {
|
||||
showStep('code');
|
||||
document.getElementById('input-code').focus();
|
||||
}
|
||||
} catch (e) {
|
||||
showError('error-email', 'Verbindungsfehler');
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Zugangscode anfordern';
|
||||
}
|
||||
|
||||
async function verifyCode() {
|
||||
var code = document.getElementById('input-code').value.trim();
|
||||
var email = document.getElementById('input-email').value.trim();
|
||||
if (!code || code.length !== 6) return;
|
||||
var btn = document.getElementById('btn-verify');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Pruefe...';
|
||||
document.getElementById('error-code').style.display = 'none';
|
||||
|
||||
try {
|
||||
var resp = await fetch('/api/auth/verify-code', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email, code: code }),
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
showError('error-code', data.detail || 'Fehler');
|
||||
} else {
|
||||
localStorage.setItem('globe_token', data.token);
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (e) {
|
||||
showError('error-code', 'Verbindungsfehler');
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Verifizieren';
|
||||
}
|
||||
|
||||
// Enter-Taste
|
||||
document.getElementById('input-email').addEventListener('keydown', function(e) { if (e.key === 'Enter') requestLink(); });
|
||||
document.getElementById('input-code').addEventListener('keydown', function(e) { if (e.key === 'Enter') verifyCode(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren