Auth: Nur noch Magic Link, Code-Verifizierung entfernt
- /api/auth/verify-code Endpoint entfernt - generate_magic_code() und VerifyCodeRequest entfernt - VerifyCodeLimiter (Brute-Force-Schutz) entfernt (nicht mehr noetig) - E-Mail-Template: 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:
@@ -1,6 +1,5 @@
|
|||||||
"""JWT-Authentifizierung mit Magic-Link-Support und Multi-Tenancy."""
|
"""JWT-Authentifizierung mit Magic-Link-Support und Multi-Tenancy."""
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from jose import jwt, JWTError
|
from jose import jwt, JWTError
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
@@ -77,7 +76,3 @@ def generate_magic_token() -> str:
|
|||||||
"""Generiert einen 64-Zeichen URL-safe Token."""
|
"""Generiert einen 64-Zeichen URL-safe Token."""
|
||||||
return secrets.token_urlsafe(48)
|
return secrets.token_urlsafe(48)
|
||||||
|
|
||||||
|
|
||||||
def generate_magic_code() -> str:
|
|
||||||
"""Generiert einen 6-stelligen numerischen Code."""
|
|
||||||
return ''.join(secrets.choice(string.digits) for _ in range(6))
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""In-Memory Rate-Limiting fuer Magic-Link-Anfragen und Code-Verifizierung."""
|
"""In-Memory Rate-Limiting fuer Magic-Link-Anfragen."""
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
@@ -51,52 +51,5 @@ class RateLimiter:
|
|||||||
self._ip_requests[ip].append(now)
|
self._ip_requests[ip].append(now)
|
||||||
|
|
||||||
|
|
||||||
class VerifyCodeLimiter:
|
# Singleton-Instanz
|
||||||
"""Rate-Limiter fuer Code-Verifizierung (Brute-Force-Schutz).
|
|
||||||
|
|
||||||
Zaehlt Fehlversuche pro E-Mail und pro IP.
|
|
||||||
Nach max_attempts wird gesperrt bis das Zeitfenster ablaeuft.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
max_attempts_per_email: int = 5,
|
|
||||||
max_attempts_per_ip: int = 15,
|
|
||||||
window_seconds: int = 600, # 10 Minuten (= Magic-Link-Ablaufzeit)
|
|
||||||
):
|
|
||||||
self.max_per_email = max_attempts_per_email
|
|
||||||
self.max_per_ip = max_attempts_per_ip
|
|
||||||
self.window = window_seconds
|
|
||||||
self._email_failures: dict[str, list[float]] = defaultdict(list)
|
|
||||||
self._ip_failures: dict[str, list[float]] = defaultdict(list)
|
|
||||||
|
|
||||||
def _clean(self, entries: list[float]) -> list[float]:
|
|
||||||
cutoff = time.time() - self.window
|
|
||||||
return [t for t in entries if t > cutoff]
|
|
||||||
|
|
||||||
def check(self, email: str, ip: str) -> tuple[bool, str]:
|
|
||||||
"""Prueft ob ein Verifizierungsversuch erlaubt ist."""
|
|
||||||
self._email_failures[email] = self._clean(self._email_failures[email])
|
|
||||||
if len(self._email_failures[email]) >= self.max_per_email:
|
|
||||||
return False, "Zu viele Fehlversuche. Bitte neuen Code anfordern."
|
|
||||||
|
|
||||||
self._ip_failures[ip] = self._clean(self._ip_failures[ip])
|
|
||||||
if len(self._ip_failures[ip]) >= self.max_per_ip:
|
|
||||||
return False, "Zu viele Fehlversuche von dieser IP-Adresse."
|
|
||||||
|
|
||||||
return True, ""
|
|
||||||
|
|
||||||
def record_failure(self, email: str, ip: str):
|
|
||||||
"""Zeichnet einen fehlgeschlagenen Versuch auf."""
|
|
||||||
now = time.time()
|
|
||||||
self._email_failures[email].append(now)
|
|
||||||
self._ip_failures[ip].append(now)
|
|
||||||
|
|
||||||
def clear(self, email: str):
|
|
||||||
"""Loescht Zaehler nach erfolgreichem Login."""
|
|
||||||
self._email_failures.pop(email, None)
|
|
||||||
|
|
||||||
|
|
||||||
# Singleton-Instanzen
|
|
||||||
magic_link_limiter = RateLimiter()
|
magic_link_limiter = RateLimiter()
|
||||||
verify_code_limiter = VerifyCodeLimiter()
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""HTML-E-Mail-Vorlagen fuer Magic Links, Einladungen und Benachrichtigungen."""
|
"""HTML-E-Mail-Vorlagen fuer Magic Links, Einladungen und Benachrichtigungen."""
|
||||||
|
|
||||||
|
|
||||||
def magic_link_login_email(username: str, code: str, link: str) -> tuple[str, str]:
|
def magic_link_login_email(username: str, link: str) -> tuple[str, str]:
|
||||||
"""Erzeugt Login-E-Mail mit Magic Link und Code.
|
"""Erzeugt Login-E-Mail mit Magic Link.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(subject, html_body)
|
(subject, html_body)
|
||||||
@@ -17,16 +17,15 @@ def magic_link_login_email(username: str, code: str, link: str) -> tuple[str, st
|
|||||||
|
|
||||||
<p style="margin: 0 0 16px 0;">Hallo {username},</p>
|
<p style="margin: 0 0 16px 0;">Hallo {username},</p>
|
||||||
|
|
||||||
<p style="margin: 0 0 24px 0;">Klicken Sie auf den Link oder geben Sie den Code ein, um sich anzumelden:</p>
|
<p style="margin: 0 0 24px 0;">Klicken Sie auf den Button, um sich anzumelden:</p>
|
||||||
|
|
||||||
<div style="background: #0f172a; border-radius: 8px; padding: 20px; text-align: center; margin: 0 0 24px 0;">
|
|
||||||
<div style="font-size: 32px; font-weight: 700; letter-spacing: 8px; color: #f0b429; font-family: monospace;">{code}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align: center; margin: 0 0 24px 0;">
|
<div style="text-align: center; margin: 0 0 24px 0;">
|
||||||
<a href="{link}" style="display: inline-block; background: #f0b429; color: #0f172a; padding: 12px 32px; border-radius: 6px; text-decoration: none; font-weight: 600;">Jetzt anmelden</a>
|
<a href="{link}" style="display: inline-block; background: #f0b429; color: #0f172a; padding: 14px 40px; border-radius: 6px; text-decoration: none; font-weight: 600; font-size: 16px;">Jetzt anmelden</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p style="color: #94a3b8; font-size: 13px; margin: 0 0 12px 0;">Oder kopieren Sie diesen Link in Ihren Browser:</p>
|
||||||
|
<p style="color: #64748b; font-size: 11px; word-break: break-all; margin: 0 0 24px 0;">{link}</p>
|
||||||
|
|
||||||
<p style="color: #94a3b8; font-size: 13px; margin: 0;">Dieser Link ist 10 Minuten gueltig. Falls Sie diese Anmeldung nicht angefordert haben, ignorieren Sie diese E-Mail.</p>
|
<p style="color: #94a3b8; font-size: 13px; margin: 0;">Dieser Link ist 10 Minuten gueltig. Falls Sie diese Anmeldung nicht angefordert haben, ignorieren Sie diese E-Mail.</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@@ -54,10 +53,10 @@ def incident_notification_email(
|
|||||||
subject = f"AegisSight - {incident_title}"
|
subject = f"AegisSight - {incident_title}"
|
||||||
|
|
||||||
icon_map = {
|
icon_map = {
|
||||||
"success": "✓", # Haekchen
|
"success": "✓",
|
||||||
"warning": "⚠", # Warndreieck
|
"warning": "⚠",
|
||||||
"error": "✗", # Kreuz
|
"error": "✗",
|
||||||
"info": "ⓘ", # Info-Kreis
|
"info": "ⓘ",
|
||||||
}
|
}
|
||||||
color_map = {
|
color_map = {
|
||||||
"success": "#22c55e",
|
"success": "#22c55e",
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ class VerifyTokenRequest(BaseModel):
|
|||||||
token: str
|
token: str
|
||||||
|
|
||||||
|
|
||||||
class VerifyCodeRequest(BaseModel):
|
|
||||||
email: str = Field(min_length=1, max_length=254)
|
|
||||||
code: str = Field(min_length=6, max_length=6)
|
|
||||||
|
|
||||||
|
|
||||||
class TokenResponse(BaseModel):
|
class TokenResponse(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from models import (
|
|||||||
MagicLinkRequest,
|
MagicLinkRequest,
|
||||||
MagicLinkResponse,
|
MagicLinkResponse,
|
||||||
VerifyTokenRequest,
|
VerifyTokenRequest,
|
||||||
VerifyCodeRequest,
|
|
||||||
TokenResponse,
|
TokenResponse,
|
||||||
UserMeResponse,
|
UserMeResponse,
|
||||||
)
|
)
|
||||||
@@ -14,13 +13,12 @@ from auth import (
|
|||||||
create_token,
|
create_token,
|
||||||
get_current_user,
|
get_current_user,
|
||||||
generate_magic_token,
|
generate_magic_token,
|
||||||
generate_magic_code,
|
|
||||||
)
|
)
|
||||||
from database import db_dependency
|
from database import db_dependency
|
||||||
from config import TIMEZONE, MAGIC_LINK_EXPIRE_MINUTES, MAGIC_LINK_BASE_URL
|
from config import TIMEZONE, MAGIC_LINK_EXPIRE_MINUTES, MAGIC_LINK_BASE_URL
|
||||||
from email_utils.sender import send_email
|
from email_utils.sender import send_email
|
||||||
from email_utils.templates import magic_link_login_email
|
from email_utils.templates import magic_link_login_email
|
||||||
from email_utils.rate_limiter import magic_link_limiter, verify_code_limiter
|
from email_utils.rate_limiter import magic_link_limiter
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
|
||||||
logger = logging.getLogger("osint.auth")
|
logger = logging.getLogger("osint.auth")
|
||||||
@@ -34,7 +32,7 @@ async def request_magic_link(
|
|||||||
request: Request,
|
request: Request,
|
||||||
db: aiosqlite.Connection = Depends(db_dependency),
|
db: aiosqlite.Connection = Depends(db_dependency),
|
||||||
):
|
):
|
||||||
"""Magic Link anfordern. Sendet E-Mail mit Link + Code."""
|
"""Magic Link anfordern. Sendet E-Mail mit Link."""
|
||||||
email = data.email.lower().strip()
|
email = data.email.lower().strip()
|
||||||
ip = request.client.host if request.client else "unknown"
|
ip = request.client.host if request.client else "unknown"
|
||||||
|
|
||||||
@@ -42,7 +40,6 @@ async def request_magic_link(
|
|||||||
allowed, reason = magic_link_limiter.check(email, ip)
|
allowed, reason = magic_link_limiter.check(email, ip)
|
||||||
if not allowed:
|
if not allowed:
|
||||||
logger.warning(f"Rate-Limit fuer {email} von {ip}: {reason}")
|
logger.warning(f"Rate-Limit fuer {email} von {ip}: {reason}")
|
||||||
# Trotzdem 200 zurueckgeben (kein Information-Leak)
|
|
||||||
return MagicLinkResponse(message="Wenn ein Konto existiert, wurde eine E-Mail gesendet.")
|
return MagicLinkResponse(message="Wenn ein Konto existiert, wurde eine E-Mail gesendet.")
|
||||||
|
|
||||||
# Nutzer suchen
|
# Nutzer suchen
|
||||||
@@ -75,9 +72,8 @@ async def request_magic_link(
|
|||||||
magic_link_limiter.record(email, ip)
|
magic_link_limiter.record(email, ip)
|
||||||
return MagicLinkResponse(message="Wenn ein Konto existiert, wurde eine E-Mail gesendet.")
|
return MagicLinkResponse(message="Wenn ein Konto existiert, wurde eine E-Mail gesendet.")
|
||||||
|
|
||||||
# Token + Code generieren
|
# Token generieren
|
||||||
token = generate_magic_token()
|
token = generate_magic_token()
|
||||||
code = generate_magic_code()
|
|
||||||
expires_at = (datetime.now(TIMEZONE) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES)).strftime('%Y-%m-%d %H:%M:%S')
|
expires_at = (datetime.now(TIMEZONE) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES)).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
# Alte ungenutzte Magic Links fuer diese E-Mail invalidieren
|
# Alte ungenutzte Magic Links fuer diese E-Mail invalidieren
|
||||||
@@ -89,14 +85,14 @@ async def request_magic_link(
|
|||||||
# Neuen Magic Link speichern
|
# Neuen Magic Link speichern
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""INSERT INTO magic_links (email, token, code, purpose, user_id, expires_at, ip_address)
|
"""INSERT INTO magic_links (email, token, code, purpose, user_id, expires_at, ip_address)
|
||||||
VALUES (?, ?, ?, 'login', ?, ?, ?)""",
|
VALUES (?, ?, '', 'login', ?, ?, ?)""",
|
||||||
(email, token, code, user["id"], expires_at, ip),
|
(email, token, user["id"], expires_at, ip),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# E-Mail senden
|
# E-Mail senden
|
||||||
link = f"{MAGIC_LINK_BASE_URL}/?token={token}"
|
link = f"{MAGIC_LINK_BASE_URL}/?token={token}"
|
||||||
subject, html = magic_link_login_email(user["email"].split("@")[0], code, link)
|
subject, html = magic_link_login_email(user["email"].split("@")[0], link)
|
||||||
await send_email(email, subject, html)
|
await send_email(email, subject, html)
|
||||||
|
|
||||||
magic_link_limiter.record(email, ip)
|
magic_link_limiter.record(email, ip)
|
||||||
@@ -160,84 +156,6 @@ async def verify_magic_link(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/verify-code", response_model=TokenResponse)
|
|
||||||
async def verify_magic_code(
|
|
||||||
data: VerifyCodeRequest,
|
|
||||||
request: Request,
|
|
||||||
db: aiosqlite.Connection = Depends(db_dependency),
|
|
||||||
):
|
|
||||||
"""Magic Code verifizieren (6-stelliger Code + E-Mail)."""
|
|
||||||
email = data.email.lower().strip()
|
|
||||||
ip = request.client.host if request.client else "unknown"
|
|
||||||
|
|
||||||
# Brute-Force-Schutz: Fehlversuche pruefen
|
|
||||||
allowed, reason = verify_code_limiter.check(email, ip)
|
|
||||||
if not allowed:
|
|
||||||
logger.warning(f"Verify-Code Rate-Limit fuer {email} von {ip}: {reason}")
|
|
||||||
# Bei Sperre alle offenen Magic Links fuer diese E-Mail invalidieren
|
|
||||||
await db.execute(
|
|
||||||
"UPDATE magic_links SET is_used = 1 WHERE email = ? AND is_used = 0",
|
|
||||||
(email,),
|
|
||||||
)
|
|
||||||
await db.commit()
|
|
||||||
raise HTTPException(status_code=429, detail=reason)
|
|
||||||
|
|
||||||
cursor = await db.execute(
|
|
||||||
"""SELECT ml.*, u.username, u.email as user_email, u.role, u.organization_id, u.is_active,
|
|
||||||
o.slug as org_slug, o.is_active as org_active
|
|
||||||
FROM magic_links ml
|
|
||||||
JOIN users u ON u.id = ml.user_id
|
|
||||||
JOIN organizations o ON o.id = u.organization_id
|
|
||||||
WHERE LOWER(ml.email) = ? AND ml.code = ? AND ml.is_used = 0
|
|
||||||
ORDER BY ml.created_at DESC LIMIT 1""",
|
|
||||||
(email, data.code),
|
|
||||||
)
|
|
||||||
ml = await cursor.fetchone()
|
|
||||||
|
|
||||||
if not ml:
|
|
||||||
verify_code_limiter.record_failure(email, ip)
|
|
||||||
logger.warning(f"Fehlgeschlagener Code-Versuch fuer {email} von {ip}")
|
|
||||||
raise HTTPException(status_code=400, detail="Ungueltiger Code")
|
|
||||||
|
|
||||||
# Ablauf pruefen
|
|
||||||
now = datetime.now(TIMEZONE)
|
|
||||||
expires = datetime.fromisoformat(ml["expires_at"])
|
|
||||||
if expires.tzinfo is None:
|
|
||||||
expires = expires.replace(tzinfo=TIMEZONE)
|
|
||||||
if now > expires:
|
|
||||||
raise HTTPException(status_code=400, detail="Code abgelaufen. Bitte neuen Code anfordern.")
|
|
||||||
|
|
||||||
if not ml["is_active"] or not ml["org_active"]:
|
|
||||||
raise HTTPException(status_code=403, detail="Konto oder Organisation deaktiviert")
|
|
||||||
|
|
||||||
# Magic Link als verwendet markieren
|
|
||||||
await db.execute("UPDATE magic_links SET is_used = 1 WHERE id = ?", (ml["id"],))
|
|
||||||
|
|
||||||
# Letzten Login aktualisieren
|
|
||||||
await db.execute(
|
|
||||||
"UPDATE users SET last_login_at = ? WHERE id = ?",
|
|
||||||
(now.isoformat(), ml["user_id"]),
|
|
||||||
)
|
|
||||||
await db.commit()
|
|
||||||
|
|
||||||
# Fehlversuche-Zaehler nach Erfolg zuruecksetzen
|
|
||||||
verify_code_limiter.clear(email)
|
|
||||||
|
|
||||||
token = create_token(
|
|
||||||
user_id=ml["user_id"],
|
|
||||||
username=ml["username"],
|
|
||||||
email=ml["user_email"],
|
|
||||||
role=ml["role"],
|
|
||||||
tenant_id=ml["organization_id"],
|
|
||||||
org_slug=ml["org_slug"],
|
|
||||||
)
|
|
||||||
|
|
||||||
return TokenResponse(
|
|
||||||
access_token=token,
|
|
||||||
username=ml["username"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me", response_model=UserMeResponse)
|
@router.get("/me", response_model=UserMeResponse)
|
||||||
async def get_me(
|
async def get_me(
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
|
|||||||
@@ -35,20 +35,20 @@
|
|||||||
<button type="submit" class="btn btn-primary btn-full" id="email-btn">Anmelden</button>
|
<button type="submit" class="btn btn-primary btn-full" id="email-btn">Anmelden</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Schritt 2: Code eingeben -->
|
<!-- Schritt 2: Link gesendet -->
|
||||||
<form id="code-form" style="display:none;">
|
<div id="link-sent" style="display:none;">
|
||||||
<p style="color: var(--text-secondary); margin: 0 0 16px 0; font-size: 14px;">
|
<div style="text-align:center; padding: 20px 0;">
|
||||||
Ein 6-stelliger Code wurde an <strong id="sent-email"></strong> gesendet.
|
<div style="font-size: 40px; margin-bottom: 16px;">✉</div>
|
||||||
</p>
|
<p style="color: var(--text-secondary); margin: 0 0 8px 0; font-size: 14px;">
|
||||||
<div class="form-group">
|
Ein Anmelde-Link wurde an
|
||||||
<label for="code">Code eingeben</label>
|
</p>
|
||||||
<input type="text" id="code" name="code" autocomplete="one-time-code" required aria-required="true"
|
<p style="color: var(--accent); font-weight: 600; font-size: 16px; margin: 0 0 16px 0;" id="sent-email"></p>
|
||||||
placeholder="000000" maxlength="6" pattern="[0-9]{6}"
|
<p style="color: var(--text-secondary); margin: 0 0 24px 0; font-size: 14px;">
|
||||||
style="text-align:center; font-size:24px; letter-spacing:8px; font-family:monospace;">
|
gesendet. Bitte prüfen Sie Ihr Postfach und klicken Sie auf den Link.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-full" id="code-btn">Verifizieren</button>
|
<button type="button" class="btn btn-secondary btn-full" id="back-btn">Andere E-Mail verwenden</button>
|
||||||
<button type="button" class="btn btn-secondary btn-full" id="back-btn" style="margin-top:8px;">Zurück</button>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
<div style="text-align:center;margin-top:16px;">
|
<div style="text-align:center;margin-top:16px;">
|
||||||
<button class="btn btn-secondary btn-small theme-toggle-btn" id="theme-toggle" onclick="ThemeManager.toggle()" title="Theme wechseln" aria-label="Theme wechseln">☼</button>
|
<button class="btn btn-secondary btn-small theme-toggle-btn" id="theme-toggle" onclick="ThemeManager.toggle()" title="Theme wechseln" aria-label="Theme wechseln">☼</button>
|
||||||
@@ -148,11 +148,10 @@
|
|||||||
throw new Error(data.detail || 'Anfrage fehlgeschlagen');
|
throw new Error(data.detail || 'Anfrage fehlgeschlagen');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zu Code-Eingabe wechseln
|
// Link-gesendet-Hinweis anzeigen
|
||||||
document.getElementById('email-form').style.display = 'none';
|
document.getElementById('email-form').style.display = 'none';
|
||||||
document.getElementById('code-form').style.display = 'block';
|
document.getElementById('link-sent').style.display = 'block';
|
||||||
document.getElementById('sent-email').textContent = currentEmail;
|
document.getElementById('sent-email').textContent = currentEmail;
|
||||||
document.getElementById('code').focus();
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorEl.textContent = err.message;
|
errorEl.textContent = err.message;
|
||||||
@@ -163,49 +162,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schritt 2: Code verifizieren
|
|
||||||
document.getElementById('code-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const errorEl = document.getElementById('login-error');
|
|
||||||
const btn = document.getElementById('code-btn');
|
|
||||||
errorEl.style.display = 'none';
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Wird geprüft...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/auth/verify-code', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: currentEmail,
|
|
||||||
code: document.getElementById('code').value.trim(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
throw new Error(data.detail || 'Verifizierung fehlgeschlagen');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
localStorage.setItem('osint_token', data.access_token);
|
|
||||||
localStorage.setItem('osint_username', data.username);
|
|
||||||
window.location.href = '/dashboard';
|
|
||||||
} catch (err) {
|
|
||||||
errorEl.textContent = err.message;
|
|
||||||
errorEl.style.display = 'block';
|
|
||||||
} finally {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Verifizieren';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Zurück-Button
|
// Zurück-Button
|
||||||
document.getElementById('back-btn').addEventListener('click', () => {
|
document.getElementById('back-btn').addEventListener('click', () => {
|
||||||
document.getElementById('code-form').style.display = 'none';
|
document.getElementById('link-sent').style.display = 'none';
|
||||||
document.getElementById('email-form').style.display = 'block';
|
document.getElementById('email-form').style.display = 'block';
|
||||||
document.getElementById('login-error').style.display = 'none';
|
document.getElementById('login-error').style.display = 'none';
|
||||||
document.getElementById('code').value = '';
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren