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
Dieser Commit ist enthalten in:
Claude Dev
2026-03-25 00:01:21 +01:00
Ursprung bd2c274dd2
Commit af08fa6b4d
5 geänderte Dateien mit 69 neuen und 183 gelöschten Zeilen

110
CLAUDE.md
Datei anzeigen

@@ -29,7 +29,7 @@ Inspiriert vom WorldView/God's Eye Konzept, komplett eigenentwickelt.
| Layer | Quelle | API | Refresh | Menge | | Layer | Quelle | API | Refresh | Menge |
|-------|--------|-----|---------|-------| |-------|--------|-----|---------|-------|
| Flugverkehr | adsb.lol | /v2/point/30/0/10000 | 15s | ~10.800 | | 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+ | | Schiffsverkehr | AISStream.io | WebSocket wss://stream.aisstream.io | Echtzeit | ~15.000+ |
| Erdbeben | USGS | earthquake.usgs.gov GeoJSON | 5min | ~50/Tag | | Erdbeben | USGS | earthquake.usgs.gov GeoJSON | 5min | ~50/Tag |
| Katastrophen | NASA EONET | eonet.gsfc.nasa.gov/api/v3 | 10min | ~30-80 aktiv | | 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 ## API-Keys & Credentials
Alle in ~/AegisSight-Globe/.env (nicht in Git): Alle in ~/AegisSight-Globe/.env (nicht in Git):
- AISSTREAM_KEY: AISStream.io WebSocket API - AISSTREAM_KEY AISStream.io WebSocket API
- CESIUM_ION_TOKEN: Cesium Ion (Free-Tier, Commercial noetig fuer Vertrieb) - CESIUM_ION_TOKEN Cesium Ion (Free-Tier, Commercial noetig fuer Vertrieb)
- JWT_SECRET: Gleicher wie Monitor - JWT_SECRET Gleicher wie Monitor
- SMTP_*: E-Mail-Versand fuer Magic Link - SMTP_* E-Mail-Versand fuer Magic Link
- MONITOR_API_KEY: Public API Key des Monitors - MONITOR_API_KEY Public API Key des Monitors
- DISASTER_INCIDENT_ID=45: Naturkatastrophen-Lage fuer Auto-Push - DISASTER_INCIDENT_ID=45 Naturkatastrophen-Lage fuer Auto-Push
## Verzeichnisstruktur ## 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 ## Bidirektionale Monitor-Verbindung
### Globe -> Monitor (Auto-Push, data_push.py) ### Globe -> Monitor (Auto-Push)
- Alle 10min: NASA EONET + USGS M4.5+ als Artikel an Monitor-Lage 45 - data_push.py sendet alle 10min NASA EONET + USGS M4.5+ an Monitor
- Duplikat-Check per Headline, Locations mit Koordinaten - Ziel-Lage: ID 45 (Naturkatastrophen international)
- Duplikat-Check per Headline im Monitor
- Monitor verifiziert und erstellt Zusammenfassung - Monitor verifiziert und erstellt Zusammenfassung
### Monitor -> Globe (Feed, data_monitor.py) ### Monitor -> Globe (Feed)
- /api/public/globe-feed?incident_id=X liefert GeoJSON - /api/public/globe-feed?incident_id=X liefert GeoJSON
- Pro Standort: Ortsspezifische Artikel (Headline, Quelle, Auszug, Datum) - Pro Standort: Ortsspezifische Artikel (Headline, Quelle, Auszug, Datum)
- Lage-Auswahl im Globe-Header bestimmt welche Daten angezeigt werden - Lage-Auswahl im Globe-Header bestimmt welche Daten angezeigt werden
@@ -119,24 +78,27 @@ Alle in ~/AegisSight-Globe/.env (nicht in Git):
### Klick-Flow ### Klick-Flow
1. User waehlt Lage im Header (z.B. Naturkatastrophen) 1. User waehlt Lage im Header (z.B. Naturkatastrophen)
2. Monitor-Standorte erscheinen als rote Punkte 2. Monitor-Standorte erscheinen als rote Punkte auf dem Globus
3. Klick auf Katastrophe zeigt NASA-Daten + passende Monitor-Artikel 3. Klick auf Katastrophe zeigt: NASA-Daten + passende Monitor-Artikel
4. Zuordnung ist ortsspezifisch (Kenia-Klick = Kenia-Artikel) 4. Artikel sind ortsspezifisch (Kenia-Klick = Kenia-Artikel, nicht global)
## Auth-System ## 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 - 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 - Alle Daten-APIs hinter Auth-Middleware
- Akzeptiert auch Monitor-JWT-Tokens (Kompatibilitaet)
## Rendering-Architektur ## Rendering-Architektur
### Performance (PointPrimitiveCollection, GPU-beschleunigt) ### Performance
- Weit (>5.000km): 5-Grad-Raster-Cluster mit Count-Labels - 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 - 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 ### Visual Modes
- STD: Photorealistisch - STD: Photorealistisch
@@ -146,23 +108,19 @@ Alle in ~/AegisSight-Globe/.env (nicht in Git):
## Bekannte Limitierungen ## 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) - Satelliten-Positionen: Vereinfachte Kepler-Berechnung (nicht SGP4-exakt)
- AISStream: ~1-2min bis globale Abdeckung nach Restart - AISStream: Schiffsdaten brauchen ~1-2min bis globale Abdeckung nach Restart
- Sentinel-2: Wolkenfreie Komposite, nicht tagesaktuell - 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 ## Entwicklungshinweise
- Alle JS als statische Dateien (kein Build-Step) - Alle JS-Dateien werden als statische Dateien ausgeliefert (kein Build-Step)
- Cache-Busting: Ctrl+Shift+R im Browser - Cache-Busting: Browser-Cache manuell mit Ctrl+Shift+R leeren
- JS-Syntax pruefen: node --check static/js/layers/DATEI.js - JS-Syntax pruefen: node --check ~/AegisSight-Globe/static/js/layers/DATEI.js
- Inkrementelle Patches vermeiden — Dateien komplett neu schreiben - Inkrementelle Patches vermeiden — bei groesseren Aenderungen Datei komplett neu schreiben
- .env Aenderungen erfordern Service-Restart - .env Aenderungen erfordern Service-Restart

Datei anzeigen

@@ -1,6 +1,5 @@
"""Auth: JWT + Magic Link fuer Globe.""" """Auth: JWT + Magic Link fuer Globe."""
import secrets import secrets
import string
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt from jose import JWTError, jwt
@@ -72,6 +71,3 @@ async def get_current_user(
def generate_magic_token() -> str: def generate_magic_token() -> str:
return secrets.token_urlsafe(48) return secrets.token_urlsafe(48)
def generate_magic_code() -> str:
return "".join(secrets.choice(string.digits) for _ in range(6))

Datei anzeigen

@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr 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 config import GLOBE_BASE_URL, MAGIC_LINK_EXPIRE_MINUTES
from database import get_db from database import get_db
from email_utils import send_magic_link_email from email_utils import send_magic_link_email
@@ -18,14 +18,9 @@ class LoginRequest(BaseModel):
email: EmailStr email: EmailStr
class CodeVerifyRequest(BaseModel):
email: EmailStr
code: str
@router.post("/request-link") @router.post("/request-link")
async def request_magic_link(req: LoginRequest, db=Depends(get_db)): 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() email = req.email.lower().strip()
# User pruefen # User pruefen
@@ -41,27 +36,26 @@ async def request_magic_link(req: LoginRequest, db=Depends(get_db)):
if not user["globe_access"]: if not user["globe_access"]:
raise HTTPException(status_code=403, detail="Kein Globe-Zugang. Bitte wenden Sie sich an Ihren Administrator.") 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() token = generate_magic_token()
code = generate_magic_code()
expires = datetime.now(timezone.utc) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES) expires = datetime.now(timezone.utc) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES)
await db.execute( await db.execute(
"""INSERT INTO magic_links (user_id, email, token, code, expires_at, purpose) """INSERT INTO magic_links (user_id, email, token, code, expires_at, purpose)
VALUES (?, ?, ?, ?, ?, 'globe_login')""", VALUES (?, ?, ?, '', ?, 'globe_login')""",
(user["id"], email, token, code, expires.isoformat()), (user["id"], email, token, expires.isoformat()),
) )
await db.commit() await db.commit()
link = f"{GLOBE_BASE_URL}/api/auth/verify?token={token}" link = f"{GLOBE_BASE_URL}/api/auth/verify?token={token}"
try: try:
await send_magic_link_email(email, code, link) await send_magic_link_email(email, link)
except Exception: except Exception:
raise HTTPException(status_code=502, detail="E-Mail konnte nicht gesendet werden.") raise HTTPException(status_code=502, detail="E-Mail konnte nicht gesendet werden.")
logger.info(f"Magic Link gesendet an {email}") 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") @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") @router.get("/me")
async def get_me(user: dict = Depends(get_current_user)): async def get_me(user: dict = Depends(get_current_user)):
return {"id": user["id"], "email": user["email"], "username": user["username"]} return {"id": user["id"], "email": user["email"], "username": user["username"]}

Datei anzeigen

@@ -9,21 +9,21 @@ from config import SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM_EMA
logger = logging.getLogger("globe.email") logger = logging.getLogger("globe.email")
async def send_magic_link_email(to_email: str, code: str, link: str): async def send_magic_link_email(to_email: str, link: str):
"""Sendet Magic Link + Code per E-Mail.""" """Sendet Magic Link per E-Mail."""
html = f""" html = f"""
<div style="font-family: 'Courier New', monospace; max-width: 500px; margin: 0 auto; padding: 30px; background: #0b1121; color: #e8eaf0; border-radius: 12px;"> <div style="font-family: 'Courier New', monospace; max-width: 500px; margin: 0 auto; padding: 30px; background: #0b1121; color: #e8eaf0; border-radius: 12px;">
<h2 style="color: #00ff88; font-size: 16px; letter-spacing: 2px; margin-bottom: 24px;">AEGISSIGHT GLOBE</h2> <h2 style="color: #00ff88; font-size: 16px; letter-spacing: 2px; margin-bottom: 24px;">AEGISSIGHT GLOBE</h2>
<p style="font-size: 14px; line-height: 1.6;">Dein Zugangscode:</p> <p style="font-size: 14px; line-height: 1.6;">Klicke auf den Button, um dich anzumelden:</p>
<div style="background: rgba(0,255,136,0.08); border: 1px solid rgba(0,255,136,0.2); border-radius: 8px; padding: 20px; text-align: center; margin: 20px 0;"> <div style="text-align: center; margin: 24px 0;">
<span style="font-size: 32px; font-weight: 700; letter-spacing: 8px; color: #00ff88;">{code}</span> <a href="{link}" style="display: inline-block; background: #00ff88; color: #0b1121; padding: 14px 40px; border-radius: 6px; text-decoration: none; font-weight: 700; font-size: 16px; letter-spacing: 1px;">Jetzt anmelden</a>
</div> </div>
<p style="font-size: 13px; color: rgba(255,255,255,0.6); line-height: 1.6;"> <p style="font-size: 12px; color: rgba(255,255,255,0.4); line-height: 1.6;">
Oder klicke auf diesen Link:<br> Oder kopiere diesen Link in deinen Browser:<br>
<a href="{link}" style="color: #00ff88; word-break: break-all;">{link}</a> <a href="{link}" style="color: #00ff88; word-break: break-all; font-size: 11px;">{link}</a>
</p> </p>
<p style="font-size: 11px; color: rgba(255,255,255,0.3); margin-top: 24px;"> <p style="font-size: 11px; color: rgba(255,255,255,0.3); margin-top: 24px;">
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.
</p> </p>
</div> </div>
""" """
@@ -31,8 +31,8 @@ async def send_magic_link_email(to_email: str, code: str, link: str):
msg = MIMEMultipart("alternative") msg = MIMEMultipart("alternative")
msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM_EMAIL}>" msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM_EMAIL}>"
msg["To"] = to_email msg["To"] = to_email
msg["Subject"] = "AegisSight Globe — Zugangscode" msg["Subject"] = "AegisSight Globe — Anmelde-Link"
msg.attach(MIMEText(f"Dein Globe-Zugangscode: {code}\n\nLink: {link}\n\nGueltig fuer 10 Minuten.", "plain")) msg.attach(MIMEText(f"Dein Globe-Anmeldelink:\n\n{link}\n\nGueltig fuer 10 Minuten.", "plain"))
msg.attach(MIMEText(html, "html")) msg.attach(MIMEText(html, "html"))
try: try:

Datei anzeigen

@@ -15,12 +15,11 @@
.logo p { font-size:11px; color:var(--text-dim); margin-top:4px; } .logo p { font-size:11px; color:var(--text-dim); margin-top:4px; }
.form-group { margin-bottom:20px; } .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; } 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); 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; 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); } input:focus { border-color:var(--accent); }
.code-input { text-align:center; letter-spacing:8px; font-size:24px; font-weight:700; }
.btn { .btn {
width:100%; padding:12px; background:rgba(0,255,136,0.1); border:1px solid var(--accent); 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; 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:hover { background:rgba(0,255,136,0.2); }
.btn:disabled { opacity:0.4; cursor:not-allowed; } .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; } .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 { display:none; }
.step.active { display:block; } .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> </style>
</head> </head>
<body> <body>
@@ -52,20 +50,21 @@
<label>E-Mail-Adresse</label> <label>E-Mail-Adresse</label>
<input type="email" id="input-email" placeholder="name@beispiel.de" autofocus> <input type="email" id="input-email" placeholder="name@beispiel.de" autofocus>
</div> </div>
<button class="btn" id="btn-send" onclick="requestLink()">Zugangscode anfordern</button> <button class="btn" id="btn-send" onclick="requestLink()">Anmelde-Link anfordern</button>
<div class="error" id="error-email"></div> <div class="error" id="error-email"></div>
</div> </div>
<!-- Step 2: Code --> <!-- Step 2: Link gesendet -->
<div id="step-code" class="step"> <div id="step-sent" class="step">
<div class="form-group"> <div style="text-align:center; padding: 16px 0;">
<label>6-stelliger Zugangscode</label> <div style="font-size: 36px; margin-bottom: 16px;">&#9993;</div>
<input type="text" id="input-code" class="code-input" maxlength="6" placeholder="------" inputmode="numeric" pattern="[0-9]*"> <p style="color:var(--text-dim); font-size:13px; margin-bottom:8px;">Anmelde-Link wurde gesendet an</p>
<p style="color:var(--accent); font-size:14px; font-weight:700; margin-bottom:16px;" id="sent-email"></p>
<p style="color:var(--text-dim); font-size:12px; line-height:1.6;">
Bitte prüfe dein Postfach und klicke auf den Link in der E-Mail.
</p>
</div> </div>
<div class="success" id="success-code" style="display:block;margin-bottom:16px;">Zugangscode wurde per E-Mail gesendet.</div> <button class="btn btn-secondary" style="margin-top:20px;" onclick="showStep('email')">Andere E-Mail verwenden</button>
<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>
</div> </div>
@@ -105,48 +104,18 @@
if (!resp.ok) { if (!resp.ok) {
showError('error-email', data.detail || 'Fehler'); showError('error-email', data.detail || 'Fehler');
} else { } else {
showStep('code'); document.getElementById('sent-email').textContent = email;
document.getElementById('input-code').focus(); showStep('sent');
} }
} catch (e) { } catch (e) {
showError('error-email', 'Verbindungsfehler'); showError('error-email', 'Verbindungsfehler');
} }
btn.disabled = false; btn.disabled = false;
btn.textContent = 'Zugangscode anfordern'; btn.textContent = 'Anmelde-Link 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 // Enter-Taste
document.getElementById('input-email').addEventListener('keydown', function(e) { if (e.key === 'Enter') requestLink(); }); 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> </script>
</body> </body>
</html> </html>