Security: Passwort-Minimum, Rate Limits, Headers, Passwort-vergessen, email_verified
- Passwort-Minimum 8 Zeichen bei Register + Reset - Rate Limit auf /resend-verification (3/h) und /forgot-password (3/h) - Security-Headers: X-Frame-Options, X-Content-Type-Options, Referrer-Policy etc. - email_verified in get_current_user SELECT ergänzt - Forum: create_thread + create_post erfordern email_verified - POST /auth/forgot-password + /auth/reset-password (2h-Token, via support@) - DB-Migration: password_reset_token + password_reset_expires - Frontend: Passwort-vergessen-Modal im Login, Reset-Formular mit Passphrase-Generator - SW by-v576, APP_VER 553
This commit is contained in:
parent
82d6417d09
commit
526ff42215
8 changed files with 232 additions and 4 deletions
|
|
@ -3,6 +3,7 @@
|
|||
import os
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, Depends
|
||||
|
|
@ -77,6 +78,8 @@ async def register(data: RegisterRequest, response: Response, request: Request):
|
|||
raise HTTPException(400, "Benutzername darf keine Leerzeichen enthalten.")
|
||||
if is_username_blocked(name):
|
||||
raise HTTPException(400, "Dieser Benutzername ist nicht erlaubt.")
|
||||
if len(data.password) < 8:
|
||||
raise HTTPException(400, "Passwort muss mindestens 8 Zeichen lang sein.")
|
||||
|
||||
with db() as conn:
|
||||
if conn.execute("SELECT 1 FROM users WHERE email=?", (data.email,)).fetchone():
|
||||
|
|
@ -247,7 +250,8 @@ async def verify_email(token: str):
|
|||
|
||||
|
||||
@router.post("/resend-verification")
|
||||
async def resend_verification(user=Depends(get_current_user)):
|
||||
async def resend_verification(request: Request, user=Depends(get_current_user)):
|
||||
rl_check(request, max_requests=3, window_seconds=3600, key="resend_verify")
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT email, name, email_verified FROM users WHERE id=?", (user["id"],)
|
||||
|
|
@ -263,3 +267,65 @@ async def resend_verification(user=Depends(get_current_user)):
|
|||
)
|
||||
_send_verification_email(row["email"], row["name"], token)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
class ForgotPasswordRequest(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
class ResetPasswordRequest(BaseModel):
|
||||
token: str
|
||||
password: str
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(data: ForgotPasswordRequest, request: Request):
|
||||
rl_check(request, max_requests=3, window_seconds=3600, key="forgot_pw")
|
||||
with db() as conn:
|
||||
user = conn.execute(
|
||||
"SELECT id, name FROM users WHERE email=?", (data.email,)
|
||||
).fetchone()
|
||||
# Immer OK zurückgeben — kein User-Enumeration
|
||||
if user:
|
||||
token = secrets.token_urlsafe(32)
|
||||
expires = (datetime.utcnow() + timedelta(hours=2)).isoformat()
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"UPDATE users SET password_reset_token=?, password_reset_expires=? WHERE id=?",
|
||||
(token, expires, user["id"])
|
||||
)
|
||||
app_url = os.getenv("APP_URL", "https://banyaro.app")
|
||||
subject = "Ban Yaro — Passwort zurücksetzen"
|
||||
body = (
|
||||
f"Hallo {user['name']},\n\n"
|
||||
"du hast eine Passwort-Zurücksetzen-Anfrage gestellt.\n\n"
|
||||
f"Klicke hier um ein neues Passwort zu setzen:\n"
|
||||
f"{app_url}/#reset-password?token={token}\n\n"
|
||||
"Der Link ist 2 Stunden gültig. Falls du keine Anfrage gestellt hast, ignoriere diese Mail.\n\n"
|
||||
"Viele Grüße,\nDas Ban Yaro Team"
|
||||
)
|
||||
from routes.outreach import _send_smtp
|
||||
try:
|
||||
_send_smtp(data.email, subject, body, "support")
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(data: ResetPasswordRequest, request: Request):
|
||||
rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw")
|
||||
if len(data.password) < 8:
|
||||
raise HTTPException(400, "Passwort muss mindestens 8 Zeichen lang sein.")
|
||||
with db() as conn:
|
||||
user = conn.execute(
|
||||
"SELECT id, password_reset_expires FROM users WHERE password_reset_token=?",
|
||||
(data.token,)
|
||||
).fetchone()
|
||||
if not user:
|
||||
raise HTTPException(400, "Ungültiger oder abgelaufener Link.")
|
||||
if user["password_reset_expires"] < datetime.utcnow().isoformat():
|
||||
raise HTTPException(400, "Dieser Link ist abgelaufen. Bitte fordere einen neuen an.")
|
||||
conn.execute(
|
||||
"UPDATE users SET pw_hash=?, password_reset_token=NULL, password_reset_expires=NULL WHERE id=?",
|
||||
(hash_password(data.password), user["id"])
|
||||
)
|
||||
return {"ok": True}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue