fix(admin): secure routes, add real IP detection, SMTP header validation, and fix Next.js layout hydration mismatch
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m5s

This commit is contained in:
2026-06-01 23:16:03 +02:00
parent 6d27dc4cda
commit 6da8a85b1d
10 changed files with 1165 additions and 96 deletions

View File

@@ -173,10 +173,26 @@ def _subscription_status_str(raw) -> str:
return raw.value if hasattr(raw, "value") else str(raw)
def _get_client_ip(request: Request) -> str:
"""Get real client IP from headers or connection"""
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
real_ip = request.headers.get("X-Real-IP")
if real_ip:
return real_ip
if request.client:
return request.client.host
return "unknown"
@router.post("/login")
async def admin_login(request: AdminLoginRequest, req: Request):
"""Admin login endpoint - Returns a bearer token for authenticated admin access"""
client_ip = req.client.host if req.client else "unknown"
client_ip = _get_client_ip(req)
# Brute-force protection
now = time.time()
@@ -225,13 +241,13 @@ async def admin_logout(authorization: Optional[str] = Header(None)):
@router.get("/verify")
async def verify_admin_session(is_admin: bool = Depends(require_admin)):
async def verify_admin_session(admin_id: str = Depends(require_admin)):
"""Verify admin token is still valid"""
return {"status": "valid", "authenticated": True}
@router.get("/dashboard")
async def get_admin_dashboard(is_admin: bool = Depends(require_admin)):
async def get_admin_dashboard(admin_id: str = Depends(require_admin)):
"""Get comprehensive admin dashboard data"""
from middleware.cleanup import create_cleanup_manager
from middleware.rate_limiting import RateLimitManager, RateLimitConfig
@@ -285,7 +301,7 @@ async def get_admin_dashboard(is_admin: bool = Depends(require_admin)):
@router.get("/users")
async def get_admin_users(is_admin: bool = Depends(require_admin)):
async def get_admin_users(admin_id: str = Depends(require_admin)):
"""Liste tous les utilisateurs (base SQLite/PostgreSQL + comptes uniquement dans users.json)."""
from services.auth_service import USE_DATABASE, DATABASE_AVAILABLE, load_users
from database.connection import get_sync_session
@@ -397,7 +413,7 @@ async def get_admin_users(is_admin: bool = Depends(require_admin)):
async def patch_admin_user_tier(
user_id: str,
body: AdminUpdateUserTierRequest,
is_admin: bool = Depends(require_admin),
admin_id: str = Depends(require_admin),
):
"""Update a user's plan/tier - Admin only"""
from services.auth_service import get_user_by_id, update_user_plan
@@ -459,7 +475,7 @@ async def patch_admin_user_tier(
async def admin_reset_user_password(
user_id: str,
body: AdminResetPasswordRequest,
is_admin: bool = Depends(require_admin),
admin_id: str = Depends(require_admin),
):
"""Définit un nouveau mot de passe pour un utilisateur (sans email de réinitialisation)."""
from services.auth_service import admin_set_user_password
@@ -484,7 +500,7 @@ async def admin_reset_user_password(
@router.get("/stats")
async def get_admin_stats(is_admin: bool = Depends(require_admin)):
async def get_admin_stats(admin_id: str = Depends(require_admin)):
"""Get comprehensive admin statistics"""
from services.auth_service import USE_DATABASE, DATABASE_AVAILABLE, load_users
from services.translation_service import _translation_cache
@@ -565,7 +581,7 @@ async def get_admin_stats(is_admin: bool = Depends(require_admin)):
@router.post("/cleanup/trigger")
async def trigger_cleanup(is_admin: bool = Depends(require_admin)):
async def trigger_cleanup(admin_id: str = Depends(require_admin)):
"""Trigger manual cleanup of expired files"""
from middleware.cleanup import create_cleanup_manager
@@ -583,7 +599,7 @@ async def trigger_cleanup(is_admin: bool = Depends(require_admin)):
@router.get("/files/tracked")
async def get_tracked_files(is_admin: bool = Depends(require_admin)):
async def get_tracked_files(admin_id: str = Depends(require_admin)):
"""Get list of currently tracked files"""
from middleware.cleanup import create_cleanup_manager
@@ -593,7 +609,7 @@ async def get_tracked_files(is_admin: bool = Depends(require_admin)):
@router.post("/quota/reset")
async def reset_translation_quotas(is_admin: bool = Depends(require_admin)):
async def reset_translation_quotas(admin_id: str = Depends(require_admin)):
"""Reset monthly translation quotas for all free-tier users.
Clears Redis keys matching quota:monthly:*
"""
@@ -623,7 +639,7 @@ async def reset_translation_quotas(is_admin: bool = Depends(require_admin)):
@router.post("/config/provider")
async def update_default_provider(
provider: str = Form(...),
is_admin: bool = Depends(require_admin),
admin_id: str = Depends(require_admin),
):
"""Update the default translation provider"""
valid_providers = [
@@ -732,7 +748,7 @@ def _extract_error_code(error_message: Optional[str]) -> Optional[str]:
@router.get("/logs")
def get_admin_logs(
is_admin: str = Depends(require_admin),
admin_id: str = Depends(require_admin),
level: str = Query(default="all", pattern="^(all|error|warning|info)$"),
search: str = Query(default="", max_length=200),
page: int = Query(default=1, ge=1),
@@ -1261,6 +1277,8 @@ async def test_send_email(
username = ((smtp_body and smtp_body.username) or "").strip() or (smtp.username or "").strip() or os.getenv("SMTP_USERNAME", "").strip()
password = ((smtp_body and smtp_body.password) or "").strip() or (smtp.password or "").strip() or os.getenv("SMTP_PASSWORD", "").strip()
from_email = ((smtp_body and smtp_body.from_email) or "").strip() or (smtp.from_email or "").strip() or os.getenv("SMTP_FROM_EMAIL", "").strip() or username
if from_email:
from_email = from_email.replace("\r", "").replace("\n", "")
use_tls = (smtp_body and smtp_body.use_tls) if (smtp_body and smtp_body.use_tls is not None) else (smtp.use_tls if smtp.use_tls is not None else True)
if not from_email: