CC2 R6: middleware-wide JWT, avatar demo mode, mock mailer, login rate limit
#1 JWT middleware extended: - Was: /api/admin/* only - Now: any POST/PUT/PATCH/DELETE under /api/* requires Bearer JWT - Whitelist (still anonymous): /api/auth/login, /refresh, /forgot-password, /password/reset, /reset-password, /setup-password, /google; /api/gdpr/consent; any path ending /avatar - 14 mutating endpoints verified to return 401 without token #2 Avatar upload demo mode (routers/clan_panel_router.py): - Anonymous → returns {demo_mode:true, slika_url:null, message:'Demo mode — slika nije spremljena. Prijavite se za pravu pohranu.'}, no FS write, no DB write - Authenticated (valid JWT, allowed role) → real save as before - Auth check now uses auth.auth_v2.decode_token (proper secret + revocation) instead of the broken local _resolve_role #3 Mock mailer (auth/mailer.py): - send_email writes RFC 822 .eml to /tmp/pgz_mailbox + appends to INDEX.jsonl - send_password_reset, send_invite helpers with HR text + HTML alt - Real SMTP active when PGZ_SMTP_HOST is set (env-driven, off by default) - forgot-password and admin invite both call mailer; audit logs mail status #5 Rate limiting on /api/auth/login: - Per-user: 5 wrong attempts → 5-minute DB-backed lockout (was 5 → 15 min). Configurable via PGZ_LOGIN_LOCK_THRESHOLD/MINUTES. - Per-IP: 10 fails / 5-min sliding window in-memory → HTTP 429 Configurable via PGZ_LOGIN_IP_THRESHOLD/WINDOW_SEC. Successful login clears the IP counter. - Failed attempts respond '(N/5) — račun je zaključan na 5 minuta' - New audit actions: login.ratelimit.ip; login.fail meta now includes fails count, locked, lock_minutes #4 Live test report: 46/46 across 6 demo users — login, JWT gate on 14 mutating endpoints, public path whitelist, demo-mode avatar + real save, forgot-password e-mail to mailbox, no-leak unknown email, 5-fail lockout, 423 during lockout, audit coverage.
This commit is contained in:
+19
-4
@@ -264,17 +264,32 @@ def invite_user(uid: int, req: InviteReq, request: Request,
|
||||
meta={"email": target["email"], "note": req.note})
|
||||
invite_link = _build_link("/static/login.html?setup=1", raw_token)
|
||||
api_link = _build_link("/api/auth/setup-password", raw_token)
|
||||
# R6 #3: send invite email (mock in dev)
|
||||
mail_result = None
|
||||
if req.send_email:
|
||||
try:
|
||||
from .mailer import send_invite
|
||||
mail_result = send_invite(
|
||||
target["email"], invite_link,
|
||||
int(INVITE_TTL.total_seconds()),
|
||||
inviter=actor.get("email"),
|
||||
role=target.get("user_type"),
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[invite mail WARN] {e}")
|
||||
audit(actor["id"], "user.invite", "user", uid,
|
||||
{"email": target["email"], "send_email": req.send_email,
|
||||
"ttl_days": INVITE_TTL.days}, ip, ua)
|
||||
# NOTE: real deployment must e-mail invite_link via a mailer (M11);
|
||||
# for now, the link is returned to the admin who triggered the invite.
|
||||
"ttl_days": INVITE_TTL.days,
|
||||
"mail_sent": bool(mail_result and mail_result.get("sent")),
|
||||
"mail_mock": bool(mail_result and mail_result.get("mock"))}, ip, ua)
|
||||
return {"status": "ok", "id": uid,
|
||||
"email": target["email"],
|
||||
"invite_link": invite_link,
|
||||
"api_link": api_link,
|
||||
"expires_in_seconds": int(INVITE_TTL.total_seconds()),
|
||||
"email_sent": False}
|
||||
"email_sent": bool(mail_result and mail_result.get("sent")),
|
||||
"email_mock": bool(mail_result and mail_result.get("mock")),
|
||||
"email_file": (mail_result or {}).get("file")}
|
||||
|
||||
# ─────────────────────────── Role change ───────────────────────────
|
||||
class RoleReq(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user