From af08fa6b4dba704556ed5f6dae1763c921773219 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Wed, 25 Mar 2026 00:01:21 +0100 Subject: [PATCH] Auth: Nur noch Magic Link, Code-Verifizierung entfernt - /api/auth/verify-code Endpoint entfernt - generate_magic_code() entfernt - E-Mail: Nur noch Anmelde-Link, kein 6-stelliger Code - Login-Seite: Zeigt nach E-Mail-Eingabe Hinweis statt Code-Feld - Magic Link Token-Verifikation via URL bleibt bestehen --- CLAUDE.md | 110 ++++++++++++++------------------------------- src/auth.py | 4 -- src/auth_router.py | 51 +++------------------ src/email_utils.py | 22 ++++----- static/login.html | 65 +++++++-------------------- 5 files changed, 69 insertions(+), 183 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 224ad14..32c49ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,7 @@ Inspiriert vom WorldView/God's Eye Konzept, komplett eigenentwickelt. | Layer | Quelle | API | Refresh | Menge | |-------|--------|-----|---------|-------| | Flugverkehr | adsb.lol | /v2/point/30/0/10000 | 15s | ~10.800 | -| (Fallback) | adsb.one | /v2/point/30/0/10000 | - | ~7.000 | +| | adsb.one (Fallback) | /v2/point/30/0/10000 | - | ~7.000 | | Schiffsverkehr | AISStream.io | WebSocket wss://stream.aisstream.io | Echtzeit | ~15.000+ | | Erdbeben | USGS | earthquake.usgs.gov GeoJSON | 5min | ~50/Tag | | Katastrophen | NASA EONET | eonet.gsfc.nasa.gov/api/v3 | 10min | ~30-80 aktiv | @@ -51,67 +51,26 @@ Inspiriert vom WorldView/God's Eye Konzept, komplett eigenentwickelt. ## API-Keys & Credentials Alle in ~/AegisSight-Globe/.env (nicht in Git): -- AISSTREAM_KEY: AISStream.io WebSocket API -- CESIUM_ION_TOKEN: Cesium Ion (Free-Tier, Commercial noetig fuer Vertrieb) -- JWT_SECRET: Gleicher wie Monitor -- SMTP_*: E-Mail-Versand fuer Magic Link -- MONITOR_API_KEY: Public API Key des Monitors -- DISASTER_INCIDENT_ID=45: Naturkatastrophen-Lage fuer Auto-Push +- AISSTREAM_KEY — AISStream.io WebSocket API +- CESIUM_ION_TOKEN — Cesium Ion (Free-Tier, Commercial noetig fuer Vertrieb) +- JWT_SECRET — Gleicher wie Monitor +- SMTP_* — E-Mail-Versand fuer Magic Link +- MONITOR_API_KEY — Public API Key des Monitors +- DISASTER_INCIDENT_ID=45 — Naturkatastrophen-Lage fuer Auto-Push ## Verzeichnisstruktur -``` -~/AegisSight-Globe/ -├── .env -├── .gitignore -├── CLAUDE.md -├── requirements.txt -├── src/ -│ ├── main.py # FastAPI App, Auth-Middleware, Static Mount -│ ├── config.py # DB, JWT, SMTP Konfiguration -│ ├── database.py # aiosqlite Verbindung zur geteilten DB -│ ├── auth.py # JWT + Magic Link Token/Code -│ ├── auth_router.py # /api/auth/* Endpoints -│ ├── email_utils.py # Magic Link E-Mail-Versand -│ ├── data_flights.py # adsb.lol + adsb.one Fallback -│ ├── data_ships.py # AISStream.io WebSocket Collector -│ ├── data_quakes.py # USGS Earthquake API -│ ├── data_gdelt.py # GDELT GEO 2.0 API -│ ├── data_satellites.py # CelesTrak TLE Orbital Elements -│ ├── data_disasters.py # NASA EONET Events -│ ├── data_monitor.py # Proxy zum Monitor Public API -│ └── data_push.py # Auto-Push: Globe-Events -> Monitor -├── static/ -│ ├── index.html # CesiumJS Globe -│ ├── login.html # Magic Link Login -│ ├── css/globe.css # Dark-Theme -│ └── js/ -│ ├── app.js # Viewer, Layer-Management -│ ├── layers/ -│ │ ├── flights.js # Flugverkehr (zoom-adaptiv) -│ │ ├── ships.js # Schiffsverkehr (zoom-adaptiv) -│ │ ├── disasters.js # EONET + USGS kombiniert -│ │ ├── gdelt.js # GDELT Nachrichten -│ │ ├── satellites.js # Satelliten-Orbits -│ │ ├── weather.js # Regenradar -│ │ ├── monitor.js # Monitor OSINT-Daten -│ │ └── visualmodes.js # NVG, FLIR, CRT -│ └── ui/ -│ ├── sidebar.js # Rechte Datenpunkt-Sidebar -│ ├── search.js # Ortssuche + City Quick-Links -│ ├── imagery.js # Satellitenbilder-Switcher -│ └── crosshairs.js # Fadenkreuz + Range Rings -└── logs/globe.log -``` + ## Bidirektionale Monitor-Verbindung -### Globe -> Monitor (Auto-Push, data_push.py) -- Alle 10min: NASA EONET + USGS M4.5+ als Artikel an Monitor-Lage 45 -- Duplikat-Check per Headline, Locations mit Koordinaten +### Globe -> Monitor (Auto-Push) +- data_push.py sendet alle 10min NASA EONET + USGS M4.5+ an Monitor +- Ziel-Lage: ID 45 (Naturkatastrophen international) +- Duplikat-Check per Headline im Monitor - Monitor verifiziert und erstellt Zusammenfassung -### Monitor -> Globe (Feed, data_monitor.py) +### Monitor -> Globe (Feed) - /api/public/globe-feed?incident_id=X liefert GeoJSON - Pro Standort: Ortsspezifische Artikel (Headline, Quelle, Auszug, Datum) - Lage-Auswahl im Globe-Header bestimmt welche Daten angezeigt werden @@ -119,24 +78,27 @@ Alle in ~/AegisSight-Globe/.env (nicht in Git): ### Klick-Flow 1. User waehlt Lage im Header (z.B. Naturkatastrophen) -2. Monitor-Standorte erscheinen als rote Punkte -3. Klick auf Katastrophe zeigt NASA-Daten + passende Monitor-Artikel -4. Zuordnung ist ortsspezifisch (Kenia-Klick = Kenia-Artikel) +2. Monitor-Standorte erscheinen als rote Punkte auf dem Globus +3. Klick auf Katastrophe zeigt: NASA-Daten + passende Monitor-Artikel +4. Artikel sind ortsspezifisch (Kenia-Klick = Kenia-Artikel, nicht global) ## Auth-System -- Magic Link: E-Mail -> 6-stelliger Code -> JWT (24h) +- Magic Link Login: E-Mail -> 6-stelliger Code per Mail -> JWT - Prueft: users.is_active=1 UND users.globe_access=1 -- Globe-Zugang: An/Aus-Button im Verwaltungsportal pro User +- Globe-Zugang wird im Verwaltungsportal per An/Aus-Button gesteuert +- JWT-Tokens: 24h Gueltigkeit, akzeptiert auch Monitor-Tokens - Alle Daten-APIs hinter Auth-Middleware -- Akzeptiert auch Monitor-JWT-Tokens (Kompatibilitaet) ## Rendering-Architektur -### Performance (PointPrimitiveCollection, GPU-beschleunigt) -- Weit (>5.000km): 5-Grad-Raster-Cluster mit Count-Labels +### Performance +- Flugzeuge/Schiffe: PointPrimitiveCollection (GPU-beschleunigt) +- Zoom-adaptiv: Cluster bei Uebersicht, Einzelpunkte bei Detail +- Weit (>5.000km): 5-Grad-Raster mit Count-Labels - Mittel (1.000-5.000km): 2-Grad-Raster -- Nah (<1.000km): Einzelne Punkte, Details bei Klick +- Nah (<1.000km): Einzelne Punkte +- Katastrophen/Satelliten: Entity API (kleinere Mengen) ### Visual Modes - STD: Photorealistisch @@ -146,23 +108,19 @@ Alle in ~/AegisSight-Globe/.env (nicht in Git): ## Bekannte Limitierungen -- Cesium Ion Free-Tier: Nicht fuer kommerziellen Vertrieb +- Cesium Ion Free-Tier: Nicht fuer kommerziellen Vertrieb (Commercial License noetig) - Satelliten-Positionen: Vereinfachte Kepler-Berechnung (nicht SGP4-exakt) -- AISStream: ~1-2min bis globale Abdeckung nach Restart -- Sentinel-2: Wolkenfreie Komposite, nicht tagesaktuell +- AISStream: Schiffsdaten brauchen ~1-2min bis globale Abdeckung nach Restart +- OpenSky als Flug-Fallback: Aggressive Rate-Limits (429), daher adsb.lol als Primary + +## Services neustarten -## Services -```bash -sudo systemctl restart globe # Globe-App -sudo systemctl restart osint-monitor # Monitor (API-Aenderungen) -sudo systemctl restart verwaltungsportal # Verwaltung -``` ## Entwicklungshinweise -- Alle JS als statische Dateien (kein Build-Step) -- Cache-Busting: Ctrl+Shift+R im Browser -- JS-Syntax pruefen: node --check static/js/layers/DATEI.js -- Inkrementelle Patches vermeiden — Dateien komplett neu schreiben +- Alle JS-Dateien werden als statische Dateien ausgeliefert (kein Build-Step) +- Cache-Busting: Browser-Cache manuell mit Ctrl+Shift+R leeren +- JS-Syntax pruefen: node --check ~/AegisSight-Globe/static/js/layers/DATEI.js +- Inkrementelle Patches vermeiden — bei groesseren Aenderungen Datei komplett neu schreiben - .env Aenderungen erfordern Service-Restart diff --git a/src/auth.py b/src/auth.py index 21b8f6a..eeaa10b 100644 --- a/src/auth.py +++ b/src/auth.py @@ -1,6 +1,5 @@ """Auth: JWT + Magic Link fuer Globe.""" import secrets -import string from datetime import datetime, timedelta, timezone from jose import JWTError, jwt @@ -72,6 +71,3 @@ async def get_current_user( def generate_magic_token() -> str: return secrets.token_urlsafe(48) - -def generate_magic_code() -> str: - return "".join(secrets.choice(string.digits) for _ in range(6)) diff --git a/src/auth_router.py b/src/auth_router.py index d599fae..fcb5eaa 100644 --- a/src/auth_router.py +++ b/src/auth_router.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, EmailStr -from auth import create_token, generate_magic_token, generate_magic_code, get_current_user +from auth import create_token, generate_magic_token, get_current_user from config import GLOBE_BASE_URL, MAGIC_LINK_EXPIRE_MINUTES from database import get_db from email_utils import send_magic_link_email @@ -18,14 +18,9 @@ class LoginRequest(BaseModel): email: EmailStr -class CodeVerifyRequest(BaseModel): - email: EmailStr - code: str - - @router.post("/request-link") async def request_magic_link(req: LoginRequest, db=Depends(get_db)): - """Sendet Magic Link + Code per E-Mail.""" + """Sendet Magic Link per E-Mail.""" email = req.email.lower().strip() # User pruefen @@ -41,27 +36,26 @@ async def request_magic_link(req: LoginRequest, db=Depends(get_db)): if not user["globe_access"]: raise HTTPException(status_code=403, detail="Kein Globe-Zugang. Bitte wenden Sie sich an Ihren Administrator.") - # Magic Token + Code erzeugen + # Magic Token erzeugen token = generate_magic_token() - code = generate_magic_code() expires = datetime.now(timezone.utc) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES) await db.execute( """INSERT INTO magic_links (user_id, email, token, code, expires_at, purpose) - VALUES (?, ?, ?, ?, ?, 'globe_login')""", - (user["id"], email, token, code, expires.isoformat()), + VALUES (?, ?, ?, '', ?, 'globe_login')""", + (user["id"], email, token, expires.isoformat()), ) await db.commit() link = f"{GLOBE_BASE_URL}/api/auth/verify?token={token}" try: - await send_magic_link_email(email, code, link) + await send_magic_link_email(email, link) except Exception: raise HTTPException(status_code=502, detail="E-Mail konnte nicht gesendet werden.") logger.info(f"Magic Link gesendet an {email}") - return {"ok": True, "message": "Zugangscode wurde per E-Mail gesendet."} + return {"ok": True, "message": "Anmelde-Link wurde per E-Mail gesendet."} @router.get("/verify") @@ -101,37 +95,6 @@ async def verify_token(token: str, db=Depends(get_db)): """) -@router.post("/verify-code") -async def verify_code(req: CodeVerifyRequest, db=Depends(get_db)): - """Verifiziert 6-stelligen Code, gibt JWT zurueck.""" - email = req.email.lower().strip() - cursor = await db.execute( - """SELECT ml.id, ml.user_id, ml.expires_at, ml.is_used, - u.username, u.email, u.is_active, u.globe_access, u.role - FROM magic_links ml JOIN users u ON ml.user_id = u.id - WHERE ml.code = ? AND LOWER(u.email) = ? AND ml.purpose = 'globe_login' - ORDER BY ml.created_at DESC LIMIT 1""", - (req.code, email), - ) - row = await cursor.fetchone() - if not row: - raise HTTPException(status_code=400, detail="Ungueltiger Code.") - if row["is_used"]: - raise HTTPException(status_code=400, detail="Code wurde bereits verwendet.") - if datetime.fromisoformat(row["expires_at"]) < datetime.now(timezone.utc): - raise HTTPException(status_code=400, detail="Code ist abgelaufen.") - if not row["is_active"] or not row["globe_access"]: - raise HTTPException(status_code=403, detail="Kein Zugang.") - - await db.execute("UPDATE magic_links SET is_used = 1 WHERE id = ?", (row["id"],)) - await db.execute("UPDATE users SET last_login_at = ? WHERE id = ?", - (datetime.now(timezone.utc).isoformat(), row["user_id"])) - await db.commit() - - jwt_token = create_token(row["user_id"], row["username"], row["email"], row["role"]) - return {"ok": True, "token": jwt_token} - - @router.get("/me") async def get_me(user: dict = Depends(get_current_user)): return {"id": user["id"], "email": user["email"], "username": user["username"]} diff --git a/src/email_utils.py b/src/email_utils.py index bc7eca9..e23b533 100644 --- a/src/email_utils.py +++ b/src/email_utils.py @@ -9,21 +9,21 @@ from config import SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM_EMA logger = logging.getLogger("globe.email") -async def send_magic_link_email(to_email: str, code: str, link: str): - """Sendet Magic Link + Code per E-Mail.""" +async def send_magic_link_email(to_email: str, link: str): + """Sendet Magic Link per E-Mail.""" html = f"""

AEGISSIGHT GLOBE

-

Dein Zugangscode:

-
- {code} +

Klicke auf den Button, um dich anzumelden:

+
+ Jetzt anmelden
-

- Oder klicke auf diesen Link:
- {link} +

+ Oder kopiere diesen Link in deinen Browser:
+ {link}

- Dieser Code ist 10 Minuten gueltig. Falls du diese Anfrage nicht gesendet hast, ignoriere diese E-Mail. + Dieser Link ist 10 Minuten gueltig. Falls du diese Anfrage nicht gesendet hast, ignoriere diese E-Mail.

""" @@ -31,8 +31,8 @@ async def send_magic_link_email(to_email: str, code: str, link: str): msg = MIMEMultipart("alternative") msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM_EMAIL}>" msg["To"] = to_email - msg["Subject"] = "AegisSight Globe — Zugangscode" - msg.attach(MIMEText(f"Dein Globe-Zugangscode: {code}\n\nLink: {link}\n\nGueltig fuer 10 Minuten.", "plain")) + msg["Subject"] = "AegisSight Globe — Anmelde-Link" + msg.attach(MIMEText(f"Dein Globe-Anmeldelink:\n\n{link}\n\nGueltig fuer 10 Minuten.", "plain")) msg.attach(MIMEText(html, "html")) try: diff --git a/static/login.html b/static/login.html index 919cb24..2907174 100644 --- a/static/login.html +++ b/static/login.html @@ -15,12 +15,11 @@ .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"] { + input[type="email"] { 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; @@ -28,12 +27,11 @@ } .btn:hover { background:rgba(0,255,136,0.2); } .btn:disabled { opacity:0.4; cursor:not-allowed; } + .btn-secondary { background:none; border-color:var(--border); color:var(--text-dim); } + .btn-secondary:hover { border-color:var(--accent); color:var(--accent); } .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); } @@ -52,20 +50,21 @@
- +
- -
-
- - + +
+
+
+

Anmelde-Link wurde gesendet an

+

+

+ Bitte prüfe dein Postfach und klicke auf den Link in der E-Mail. +

-
Zugangscode wurde per E-Mail gesendet.
- -
- Andere E-Mail verwenden +
@@ -105,48 +104,18 @@ if (!resp.ok) { showError('error-email', data.detail || 'Fehler'); } else { - showStep('code'); - document.getElementById('input-code').focus(); + document.getElementById('sent-email').textContent = email; + showStep('sent'); } } 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'; + btn.textContent = 'Anmelde-Link anfordern'; } // 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(); });