diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index aac4d97..78f1906 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -48,7 +48,7 @@ from app.schemas.admin import ( UserListItem, UserListResponse, ) -from app.services.audit import log_audit_event +from app.services.audit import get_client_ip, log_audit_event from app.services.auth import hash_password # --------------------------------------------------------------------------- @@ -228,7 +228,7 @@ async def create_user( actor_id=actor.id, target_id=new_user.id, detail={"username": new_user.username, "role": new_user.role}, - ip=request.client.host if request.client else None, + ip=get_client_ip(request), ) try: @@ -290,7 +290,7 @@ async def update_user_role( actor_id=actor.id, target_id=user_id, detail={"old_role": old_role, "new_role": data.role, "sessions_revoked": revoked}, - ip=request.client.host if request.client else None, + ip=get_client_ip(request), ) await db.commit() @@ -332,7 +332,7 @@ async def reset_user_password( actor_id=actor.id, target_id=user_id, detail={"sessions_revoked": revoked}, - ip=request.client.host if request.client else None, + ip=get_client_ip(request), ) await db.commit() @@ -385,7 +385,7 @@ async def disable_user_mfa( actor_id=actor.id, target_id=user_id, detail={"sessions_revoked": revoked}, - ip=request.client.host if request.client else None, + ip=get_client_ip(request), ) await db.commit() @@ -420,7 +420,7 @@ async def toggle_mfa_enforce( actor_id=actor.id, target_id=user_id, detail={"enforce": data.enforce}, - ip=request.client.host if request.client else None, + ip=get_client_ip(request), ) await db.commit() @@ -462,7 +462,7 @@ async def toggle_user_active( actor_id=actor.id, target_id=user_id, detail={"sessions_revoked": revoked}, - ip=request.client.host if request.client else None, + ip=get_client_ip(request), ) await db.commit() @@ -494,7 +494,7 @@ async def revoke_user_sessions( actor_id=actor.id, target_id=user_id, detail={"sessions_revoked": revoked}, - ip=request.client.host if request.client else None, + ip=get_client_ip(request), ) await db.commit() @@ -545,7 +545,7 @@ async def delete_user( actor_id=actor.id, target_id=user_id, detail={"user_id": user_id, "username": deleted_username}, - ip=request.client.host if request.client else None, + ip=get_client_ip(request), ) # Flush audit + session revocation within the same transaction await db.flush() @@ -652,7 +652,7 @@ async def update_system_config( action="admin.config_updated", actor_id=actor.id, detail=changes, - ip=request.client.host if request.client else None, + ip=get_client_ip(request), ) await db.commit() diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index ba8d7bd..72cdbbc 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -43,7 +43,7 @@ from app.services.auth import ( create_mfa_token, create_mfa_enforce_token, ) -from app.services.audit import log_audit_event +from app.services.audit import get_client_ip, log_audit_event from app.config import settings as app_settings router = APIRouter() @@ -275,7 +275,7 @@ async def setup( await _create_user_defaults(db, new_user.id) - ip = request.client.host if request.client else "unknown" + ip = get_client_ip(request) user_agent = request.headers.get("user-agent") _, token = await _create_db_session(db, new_user, ip, user_agent) _set_session_cookie(response, token) @@ -307,7 +307,7 @@ async def login( HTTP 403 — account disabled (is_active=False) HTTP 423 — account locked """ - client_ip = request.client.host if request.client else "unknown" + client_ip = get_client_ip(request) result = await db.execute(select(User).where(User.username == data.username)) user = result.scalar_one_or_none() @@ -438,7 +438,7 @@ async def register( await _create_user_defaults(db, new_user.id) - ip = request.client.host if request.client else "unknown" + ip = get_client_ip(request) user_agent = request.headers.get("user-agent") await log_audit_event( diff --git a/backend/app/routers/totp.py b/backend/app/routers/totp.py index e0424da..1371bf8 100644 --- a/backend/app/routers/totp.py +++ b/backend/app/routers/totp.py @@ -35,6 +35,7 @@ from app.models.session import UserSession from app.models.totp_usage import TOTPUsage from app.models.backup_code import BackupCode from app.routers.auth import get_current_user, _set_session_cookie +from app.services.audit import get_client_ip from app.services.auth import ( verify_password_with_upgrade, hash_password, @@ -164,7 +165,7 @@ async def _create_full_session( """Create a UserSession row and return the signed cookie token.""" session_id = uuid.uuid4().hex expires_at = datetime.now() + timedelta(days=app_settings.SESSION_MAX_AGE_DAYS) - ip = request.client.host if request.client else None + ip = get_client_ip(request) user_agent = request.headers.get("user-agent") db_session = UserSession( diff --git a/backend/app/services/audit.py b/backend/app/services/audit.py index a38548c..63c80bf 100644 --- a/backend/app/services/audit.py +++ b/backend/app/services/audit.py @@ -1,8 +1,21 @@ import json +from fastapi import Request from sqlalchemy.ext.asyncio import AsyncSession from app.models.audit_log import AuditLog +def get_client_ip(request: Request) -> str: + """Extract the real client IP from proxy headers, falling back to direct connection.""" + forwarded_for = request.headers.get("x-forwarded-for") + if forwarded_for: + # X-Forwarded-For can be a comma-separated list; first entry is the original client + return forwarded_for.split(",")[0].strip() + real_ip = request.headers.get("x-real-ip") + if real_ip: + return real_ip.strip() + return request.client.host if request.client else "unknown" + + async def log_audit_event( db: AsyncSession, action: str,