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
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m5s
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user