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,4 +1,4 @@
|
||||
"""In-Memory Rate-Limiting fuer Magic-Link-Anfragen und Code-Verifizierung."""
|
||||
"""In-Memory Rate-Limiting fuer Magic-Link-Anfragen."""
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -51,52 +51,5 @@ class RateLimiter:
|
||||
self._ip_requests[ip].append(now)
|
||||
|
||||
|
||||
class VerifyCodeLimiter:
|
||||
"""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
|
||||
# Singleton-Instanz
|
||||
magic_link_limiter = RateLimiter()
|
||||
verify_code_limiter = VerifyCodeLimiter()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""HTML-E-Mail-Vorlagen fuer Magic Links, Einladungen und Benachrichtigungen."""
|
||||
|
||||
|
||||
def magic_link_login_email(username: str, code: str, link: str) -> tuple[str, str]:
|
||||
"""Erzeugt Login-E-Mail mit Magic Link und Code.
|
||||
def magic_link_login_email(username: str, link: str) -> tuple[str, str]:
|
||||
"""Erzeugt Login-E-Mail mit Magic Link.
|
||||
|
||||
Returns:
|
||||
(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 24px 0;">Klicken Sie auf den Link oder geben Sie den Code ein, 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>
|
||||
<p style="margin: 0 0 24px 0;">Klicken Sie auf den Button, um sich anzumelden:</p>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</body>
|
||||
@@ -54,10 +53,10 @@ def incident_notification_email(
|
||||
subject = f"AegisSight - {incident_title}"
|
||||
|
||||
icon_map = {
|
||||
"success": "✓", # Haekchen
|
||||
"warning": "⚠", # Warndreieck
|
||||
"error": "✗", # Kreuz
|
||||
"info": "ⓘ", # Info-Kreis
|
||||
"success": "✓",
|
||||
"warning": "⚠",
|
||||
"error": "✗",
|
||||
"info": "ⓘ",
|
||||
}
|
||||
color_map = {
|
||||
"success": "#22c55e",
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren