CC2 R5: defense-in-depth JWT + invite/reset token flows + audit
#1 JWT middleware: - pgz_sport_api.py: starlette middleware require_jwt_on_admin runs before every /api/admin/* route. Even routes that lack Depends(require_user) cannot be reached without a valid Bearer token (verifies signature, exp, typ='access', revocation via user_sessions). OPTIONS passes for CORS. #2 Invitation flow: - pgz_sport.user_action_tokens table (token_hash, user_id, kind, expires_at, used_at, created_by, ip, meta). Single-use, raw token never persisted. - POST /api/admin/users/{id}/invite — issues 'invite' token (TTL 7d), marks must_change_pwd, revokes existing sessions, returns invite_link. - GET /api/auth/setup-password?token=X — preflight (no consume). - POST /api/auth/setup-password — consumes token, sets password, sets email_verified=true. #3 Password reset flow: - POST /api/auth/forgot-password — generic 'ako račun postoji' response; issues 'reset' token (TTL 2h) only for active users. Token returned in response only on localhost or if PGZ_REVEAL_RESET_TOKEN=1. - GET /api/auth/reset-password?token=X — preflight. - POST /api/auth/reset-password — consumes token, sets new password, revokes all active sessions. #4 Audit coverage (auth events): - login.ok, login.fail (with reason), login.locked, login.2fa_required, login.2fa_fail, logout, auth.refresh, password.change, password.reset.ok, password.reset.fail, password.forgot.issue, password.forgot.miss, invite.consume.ok, invite.consume.fail, user.invite, user.create, user.update, user.delete, user.role.change, user.suspend, user.unsuspend, user.password.reset, 2fa.verify.ok, 2fa.verify.fail, 2fa.disable. #5 Live tests: 41/41 across 6 demo users (incl. fresh invited+deleted user). Phase 2 verifies 14 endpoints reject no-auth and accept valid Bearer.
This commit is contained in:
@@ -76,6 +76,39 @@ def apply_privacy(rows, admin):
|
||||
app = FastAPI(title="PGŽ Sportski savez ERP/CRM", version="1.0.0")
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
# ─── R5 #1: Defense-in-depth JWT enforcement on /api/admin/* ───
|
||||
# Even if a route accidentally lacks `Depends(require_user)`, this middleware
|
||||
# rejects requests with no/invalid Bearer token before they reach the handler.
|
||||
@app.middleware("http")
|
||||
async def require_jwt_on_admin(request, call_next):
|
||||
p = request.url.path
|
||||
# Only gate admin endpoints — leave /api/auth/*, public /api/v2/* etc. alone
|
||||
if p.startswith("/api/admin/") or p == "/api/admin":
|
||||
# OPTIONS preflight passes through
|
||||
if request.method == "OPTIONS":
|
||||
return await call_next(request)
|
||||
try:
|
||||
from auth.auth_v2 import decode_token, _is_revoked
|
||||
auth = request.headers.get("authorization", "")
|
||||
if not auth.lower().startswith("bearer "):
|
||||
from starlette.responses import JSONResponse as _JR
|
||||
return _JR({"detail": "Authentication required"}, status_code=401)
|
||||
token = auth.split(" ", 1)[1].strip()
|
||||
try:
|
||||
payload = decode_token(token)
|
||||
except Exception:
|
||||
from starlette.responses import JSONResponse as _JR
|
||||
return _JR({"detail": "Invalid or expired token"}, status_code=401)
|
||||
if payload.get("typ") not in (None, "access"):
|
||||
from starlette.responses import JSONResponse as _JR
|
||||
return _JR({"detail": "Wrong token type"}, status_code=401)
|
||||
if _is_revoked(payload.get("jti", "")):
|
||||
from starlette.responses import JSONResponse as _JR
|
||||
return _JR({"detail": "Token revoked"}, status_code=401)
|
||||
except Exception as e:
|
||||
print(f"[JWT-MW WARN] {e}")
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
# === URL rewrite middleware - convert direct external image URLs to /img-proxy ===
|
||||
import json as _json_mw
|
||||
@@ -1361,6 +1394,13 @@ try:
|
||||
except Exception as e:
|
||||
print(f'[CRM/PANEL] clan_panel router fail: {e}')
|
||||
|
||||
try:
|
||||
from crm_extras_router import router as crm_extras_router
|
||||
app.include_router(crm_extras_router)
|
||||
print('[CRM/R5] extras router loaded (bulk + xlsx + stats + notifications)')
|
||||
except Exception as e:
|
||||
print(f'[CRM/R5] extras router fail: {e}')
|
||||
|
||||
# === Round 3 / CC2 — M1 Auth + M2 Admin Users + M10 GDPR ===
|
||||
try:
|
||||
from auth.auth_v2 import router as auth_v2_router
|
||||
|
||||
Reference in New Issue
Block a user