Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions app/auth/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from pydantic import BaseModel, EmailStr
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
from typing import Optional, List, Dict, Any
from enum import Enum
from datetime import datetime


class UserSignupRequest(BaseModel):
Expand All @@ -21,6 +23,8 @@ class UserResponse(BaseModel):
last_name: str
is_active: bool
created_at: str
role: UserRole = UserRole.USER
last_login: Optional[datetime] = None


class AuthResponse(BaseModel):
Expand All @@ -36,4 +40,38 @@ class TokenResponse(BaseModel):


class RefreshTokenRequest(BaseModel):
refresh_token: str
refresh_token: str


class UserRole(str, Enum):
ADMIN = "admin"
USER = "user"
MODERATOR = "moderator"


class UserUpdateRequest(BaseModel):
first_name: Optional[str] = Field(None, min_length=1, max_length=50)
last_name: Optional[str] = Field(None, min_length=1, max_length=50)
role: Optional[UserRole] = None


class ChangePasswordRequest(BaseModel):
current_password: str
new_password: str = Field(..., min_length=8, max_length=100)
Comment on lines +58 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Password validation in model is insufficient.

Line 60 only enforces length constraints (8-100 characters) but doesn't validate password complexity. While the PasswordValidator class exists in security.py, it's not integrated here.

Consider adding a Pydantic validator to enforce password strength:

from pydantic import field_validator

class ChangePasswordRequest(BaseModel):
    current_password: str
    new_password: str = Field(..., min_length=8, max_length=100)
    
    @field_validator('new_password')
    @classmethod
    def validate_password_strength(cls, v):
        from .security import password_validator
        result = password_validator.validate_password_strength(v)
        if not result["valid"]:
            raise ValueError(f"Password validation failed: {', '.join(result['issues'])}")
        return v

This ensures validation happens at the model level, providing immediate feedback to API clients.

🤖 Prompt for AI Agents
In app/auth/models.py around lines 58 to 60, the ChangePasswordRequest model
only enforces length on new_password and doesn't run the existing
PasswordValidator; add a Pydantic field validator on 'new_password' that imports
the password validator from app.auth.security (inside the method to avoid
circular imports), calls its validate_password_strength method, and raises a
ValueError listing returned issues when validation fails so API clients get
immediate, descriptive feedback; keep existing Field length constraints.



class SessionInfo(BaseModel):
session_id: str
created_at: datetime
last_activity: datetime
ip_address: Optional[str] = None
user_agent: Optional[str] = None


class AuditLogEntry(BaseModel):
user_id: str
action: str
timestamp: datetime
ip_address: Optional[str] = None
user_agent: Optional[str] = None
details: Optional[Dict[str, Any]] = None
214 changes: 209 additions & 5 deletions app/auth/routes.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
from fastapi import APIRouter, HTTPException, status, Depends
from fastapi import APIRouter, HTTPException, status, Depends, Request, Header
from fastapi.security import HTTPBearer
from .models import (
UserSignupRequest,
UserLoginRequest,
AuthResponse,
UserResponse,
TokenResponse,
RefreshTokenRequest
RefreshTokenRequest,
UserUpdateRequest,
ChangePasswordRequest,
UserRole,
SessionInfo,
AuditLogEntry
)
from .firebase_auth import firebase_auth
from .dependencies import get_current_user
from typing import Dict, Any
from typing import Dict, Any, Optional, List
import uuid
from datetime import datetime

router = APIRouter(prefix="/auth", tags=["authentication"])

Expand Down Expand Up @@ -130,6 +137,203 @@ async def verify_token(current_user: Dict[str, Any] = Depends(get_current_user))
"user": {
"id": current_user["uid"],
"email": current_user["email"],
"role": current_user["role"]
"role": current_user.get("role", "user")
}
}
}


@router.put("/profile", response_model=UserResponse)
async def update_profile(
profile_data: UserUpdateRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
request: Request = None
):
"""
Update user profile information
"""
try:
# Update user in Firebase
update_data = {}
if profile_data.first_name is not None:
update_data["first_name"] = profile_data.first_name
if profile_data.last_name is not None:
update_data["last_name"] = profile_data.last_name

# Only admins can change roles
if profile_data.role is not None:
if current_user.get("role") != UserRole.ADMIN:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only administrators can change user roles"
)
update_data["role"] = profile_data.role.value

if update_data:
await firebase_auth.update_user(current_user["uid"], update_data)

# Log the profile update
await _log_audit_event(
user_id=current_user["uid"],
action="profile_update",
request=request,
details=update_data
)

# Return updated user info
updated_user = await firebase_auth.get_user(current_user["uid"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The code calls firebase_auth.get_user(), but this method is not defined in the FirebaseAuthService class in app/auth/firebase_auth.py. This will cause a server error (AttributeError) when this endpoint is hit. You might need to implement this method in FirebaseAuthService or use an existing method from the firebase_admin library directly, such as firebase_admin.auth.get_user().

return UserResponse(
id=updated_user["uid"],
email=updated_user["email"],
first_name=updated_user["first_name"],
last_name=updated_user["last_name"],
is_active=True,
created_at=updated_user.get("created_at", ""),
role=UserRole(updated_user.get("role", "user")),
last_login=updated_user.get("last_login")
)

except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
Comment on lines +195 to +199

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The try...except Exception block is too broad. It will catch HTTPExceptions raised for specific reasons (like the 403 Forbidden for role changes on line 165) and re-raise them as a generic 400 Bad Request. This hides the original, more specific error from the client. Additionally, returning str(e) in the detail can leak sensitive implementation details. You should catch more specific exceptions from the firebase_auth service or let FastAPI handle HTTPExceptions by not catching them here.

Comment on lines +145 to +199
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Profile update endpoint has authorization issues.

Lines 163-169: The role change authorization check compares against UserRole.ADMIN (an enum), but current_user.get("role") likely returns a string. This comparison will always fail unless the role is already the enum type.

Fix the role comparison:

         # Only admins can change roles
         if profile_data.role is not None:
-            if current_user.get("role") != UserRole.ADMIN:
+            if current_user.get("role") != UserRole.ADMIN.value:
                 raise HTTPException(
                     status_code=status.HTTP_403_FORBIDDEN,
                     detail="Only administrators can change user roles"
                 )
             update_data["role"] = profile_data.role.value

Or ensure current_user["role"] is always a UserRole enum in the authentication layer.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.put("/profile", response_model=UserResponse)
async def update_profile(
profile_data: UserUpdateRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
request: Request = None
):
"""
Update user profile information
"""
try:
# Update user in Firebase
update_data = {}
if profile_data.first_name is not None:
update_data["first_name"] = profile_data.first_name
if profile_data.last_name is not None:
update_data["last_name"] = profile_data.last_name
# Only admins can change roles
if profile_data.role is not None:
if current_user.get("role") != UserRole.ADMIN:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only administrators can change user roles"
)
update_data["role"] = profile_data.role.value
if update_data:
await firebase_auth.update_user(current_user["uid"], update_data)
# Log the profile update
await _log_audit_event(
user_id=current_user["uid"],
action="profile_update",
request=request,
details=update_data
)
# Return updated user info
updated_user = await firebase_auth.get_user(current_user["uid"])
return UserResponse(
id=updated_user["uid"],
email=updated_user["email"],
first_name=updated_user["first_name"],
last_name=updated_user["last_name"],
is_active=True,
created_at=updated_user.get("created_at", ""),
role=UserRole(updated_user.get("role", "user")),
last_login=updated_user.get("last_login")
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
# Only admins can change roles
if profile_data.role is not None:
if current_user.get("role") != UserRole.ADMIN.value:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only administrators can change user roles"
)
update_data["role"] = profile_data.role.value
🧰 Tools
🪛 Ruff (0.13.1)

148-148: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


165-168: Abstract raise to an inner function

(TRY301)


195-195: Do not catch blind exception: Exception

(BLE001)


196-199: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
In app/auth/routes.py around lines 145 to 199, the role-change authorization
check compares current_user.get("role") to the UserRole enum which will fail if
current_user["role"] is a string; update the check to either compare against the
enum's string value (e.g. current_user.get("role") == UserRole.ADMIN.value) or
convert the stored role to the enum first (e.g.
UserRole(current_user.get("role")) == UserRole.ADMIN), and ensure subsequent
logic uses the same normalized form so role checks work reliably (alternatively,
make the authentication layer return UserRole enums consistently).



@router.post("/change-password")
async def change_password(
password_data: ChangePasswordRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
request: Request = None
):
"""
Change user password
"""
try:
# Verify current password
await firebase_auth.sign_in_user(
email=current_user["email"],
password=password_data.current_password
)

# Update password
await firebase_auth.update_user_password(
current_user["uid"],
password_data.new_password
)

# Log the password change
await _log_audit_event(
user_id=current_user["uid"],
action="password_change",
request=request
)

return {"message": "Password changed successfully"}

except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid current password or unable to update password"
)
Comment on lines +233 to +237

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the update_profile endpoint, this try...except Exception block is too broad. If firebase_auth.sign_in_user fails due to an invalid password, it should likely be a 401 Unauthorized or 403 Forbidden error, not a generic 400 Bad Request. The current implementation masks the true cause of the failure and returns a misleading status code and message.

Comment on lines +202 to +237
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Password change flow has a security vulnerability.

Lines 213-216: Re-authenticating with the current password to verify it is good practice. However, line 219-222 calls update_user_password without checking password strength or validation. The ChangePasswordRequest model enforces a minimum length of 8 characters, but doesn't validate complexity.

Integrate the PasswordValidator from app/auth/security.py:

+from .security import password_validator
+
 @router.post("/change-password")
 async def change_password(
     password_data: ChangePasswordRequest,
     current_user: Dict[str, Any] = Depends(get_current_user),
     request: Request = None
 ):
     """
     Change user password
     """
     try:
         # Verify current password
         await firebase_auth.sign_in_user(
             email=current_user["email"],
             password=password_data.current_password
         )
         
+        # Validate new password strength
+        validation_result = password_validator.validate_password_strength(
+            password_data.new_password
+        )
+        if not validation_result["valid"]:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=f"Password validation failed: {', '.join(validation_result['issues'])}"
+            )
+        
         # Update password
         await firebase_auth.update_user_password(
             current_user["uid"],
             password_data.new_password
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.post("/change-password")
async def change_password(
password_data: ChangePasswordRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
request: Request = None
):
"""
Change user password
"""
try:
# Verify current password
await firebase_auth.sign_in_user(
email=current_user["email"],
password=password_data.current_password
)
# Update password
await firebase_auth.update_user_password(
current_user["uid"],
password_data.new_password
)
# Log the password change
await _log_audit_event(
user_id=current_user["uid"],
action="password_change",
request=request
)
return {"message": "Password changed successfully"}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid current password or unable to update password"
)
from .security import password_validator
@router.post("/change-password")
async def change_password(
password_data: ChangePasswordRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
request: Request = None
):
"""
Change user password
"""
try:
# Verify current password
await firebase_auth.sign_in_user(
email=current_user["email"],
password=password_data.current_password
)
# Validate new password strength
validation_result = password_validator.validate_password_strength(
password_data.new_password
)
if not validation_result["valid"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Password validation failed: {', '.join(validation_result['issues'])}"
)
# Update password
await firebase_auth.update_user_password(
current_user["uid"],
password_data.new_password
)
# Log the password change
await _log_audit_event(
user_id=current_user["uid"],
action="password_change",
request=request
)
return {"message": "Password changed successfully"}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid current password or unable to update password"
)
🧰 Tools
🪛 Ruff (0.13.1)

205-205: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


231-231: Consider moving this statement to an else block

(TRY300)


233-233: Do not catch blind exception: Exception

(BLE001)


233-233: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


234-237: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
In app/auth/routes.py around lines 202 to 237, the password-change endpoint
currently re-authenticates the user but directly updates the password without
validating complexity; import and use PasswordValidator from
app/auth/security.py to validate password_data.new_password before calling
update_user_password, and if validation fails raise
HTTPException(status_code=400) with a clear non-sensitive message (do not log
the raw password); only call update_user_password and proceed to auditing when
the validator approves the new password.



@router.get("/sessions", response_model=List[SessionInfo])
async def get_user_sessions(
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""
Get active sessions for the current user
"""
# This would typically fetch from a database
# For demo purposes, return a mock session
sessions = [
SessionInfo(
session_id=str(uuid.uuid4()),
created_at=datetime.now(),
last_activity=datetime.now(),
ip_address="127.0.0.1",
user_agent="Mozilla/5.0 (Demo Browser)"
)
]
return sessions


@router.delete("/sessions/{session_id}")
async def revoke_session(
session_id: str,
current_user: Dict[str, Any] = Depends(get_current_user),
request: Request = None
):
"""
Revoke a specific user session
"""
# This would typically remove the session from a database
await _log_audit_event(
user_id=current_user["uid"],
action="session_revoked",
request=request,
details={"session_id": session_id}
)

return {"message": f"Session {session_id} has been revoked"}


@router.get("/audit-logs", response_model=List[AuditLogEntry])
async def get_audit_logs(
current_user: Dict[str, Any] = Depends(get_current_user),
limit: int = 50
):
"""
Get audit logs for the current user (admin only)
"""
if current_user.get("role") != UserRole.ADMIN:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only administrators can view audit logs"
)

# This would typically fetch from a database
# For demo purposes, return mock audit logs
logs = [
AuditLogEntry(
user_id=current_user["uid"],
action="login",
timestamp=datetime.now(),
ip_address="127.0.0.1",
user_agent="Mozilla/5.0 (Demo Browser)"
),
AuditLogEntry(
user_id=current_user["uid"],
action="profile_update",
timestamp=datetime.now(),
ip_address="127.0.0.1",
details={"fields_updated": ["first_name"]}
)
]
return logs[:limit]
Comment on lines +281 to +313
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Audit logs endpoint has inconsistent authorization.

Line 289 checks if the current user's role equals UserRole.ADMIN, but similar to the profile update endpoint, this comparison may fail if current_user.get("role") returns a string instead of the enum.

Apply the same fix as in the profile update endpoint:

-    if current_user.get("role") != UserRole.ADMIN:
+    if current_user.get("role") != UserRole.ADMIN.value:
         raise HTTPException(
             status_code=status.HTTP_403_FORBIDDEN,
             detail="Only administrators can view audit logs"
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.get("/audit-logs", response_model=List[AuditLogEntry])
async def get_audit_logs(
current_user: Dict[str, Any] = Depends(get_current_user),
limit: int = 50
):
"""
Get audit logs for the current user (admin only)
"""
if current_user.get("role") != UserRole.ADMIN:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only administrators can view audit logs"
)
# This would typically fetch from a database
# For demo purposes, return mock audit logs
logs = [
AuditLogEntry(
user_id=current_user["uid"],
action="login",
timestamp=datetime.now(),
ip_address="127.0.0.1",
user_agent="Mozilla/5.0 (Demo Browser)"
),
AuditLogEntry(
user_id=current_user["uid"],
action="profile_update",
timestamp=datetime.now(),
ip_address="127.0.0.1",
details={"fields_updated": ["first_name"]}
)
]
return logs[:limit]
@router.get("/audit-logs", response_model=List[AuditLogEntry])
async def get_audit_logs(
current_user: Dict[str, Any] = Depends(get_current_user),
limit: int = 50
):
"""
Get audit logs for the current user (admin only)
"""
if current_user.get("role") != UserRole.ADMIN.value:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only administrators can view audit logs"
)
# This would typically fetch from a database
# For demo purposes, return mock audit logs
logs = [
AuditLogEntry(
user_id=current_user["uid"],
action="login",
timestamp=datetime.now(),
ip_address="127.0.0.1",
user_agent="Mozilla/5.0 (Demo Browser)"
),
AuditLogEntry(
user_id=current_user["uid"],
action="profile_update",
timestamp=datetime.now(),
ip_address="127.0.0.1",
details={"fields_updated": ["first_name"]}
)
]
return logs[:limit]
🧰 Tools
🪛 Ruff (0.13.1)

283-283: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
In app/auth/routes.py around lines 281 to 313, the role comparison uses
current_user.get("role") directly which may be a string and thus fail against
the UserRole enum; convert the retrieved role to the UserRole enum before
comparing (e.g., create a variable like role =
UserRole(current_user.get("role")) inside a try/except) and if conversion fails
or role is not UserRole.ADMIN raise the same HTTPException; this mirrors the
profile update endpoint fix and ensures robust enum comparison and proper error
handling for missing/invalid role values.



async def _log_audit_event(
user_id: str,
action: str,
request: Request = None,
details: Optional[Dict[str, Any]] = None
):
"""
Log an audit event
"""
# This would typically save to a database
# For demo purposes, we'll just print it
ip_address = request.client.host if request else None
user_agent = request.headers.get("user-agent") if request else None

audit_entry = AuditLogEntry(
user_id=user_id,
action=action,
timestamp=datetime.now(),
ip_address=ip_address,
user_agent=user_agent,
details=details
)

print(f"Audit Log: {audit_entry.dict()}")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The .dict() method is deprecated in Pydantic v2 (which you are using) and will be removed in v3. You should use .model_dump() instead for serialization.

Suggested change
print(f"Audit Log: {audit_entry.dict()}")
print(f"Audit Log: {audit_entry.model_dump()}")

Comment on lines +316 to +339
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Audit event helper should integrate with AuditLogger.

The _log_audit_event helper reimplements logging functionality that already exists in app/auth/security.py's AuditLogger class. This is code duplication.

Use the existing AuditLogger:

+from .security import audit_logger
+
 async def _log_audit_event(
     user_id: str,
     action: str,
     request: Request = None,
     details: Optional[Dict[str, Any]] = None
 ):
     """
     Log an audit event
     """
-    # This would typically save to a database
-    # For demo purposes, we'll just print it
     ip_address = request.client.host if request else None
     user_agent = request.headers.get("user-agent") if request else None
     
-    audit_entry = AuditLogEntry(
-        user_id=user_id,
-        action=action,
-        timestamp=datetime.now(),
-        ip_address=ip_address,
-        user_agent=user_agent,
-        details=details
+    audit_logger.log_event(
+        user_id=user_id,
+        action=action,
+        ip_address=ip_address,
+        user_agent=user_agent,
+        details=details
     )
-    
-    print(f"Audit Log: {audit_entry.dict()}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def _log_audit_event(
user_id: str,
action: str,
request: Request = None,
details: Optional[Dict[str, Any]] = None
):
"""
Log an audit event
"""
# This would typically save to a database
# For demo purposes, we'll just print it
ip_address = request.client.host if request else None
user_agent = request.headers.get("user-agent") if request else None
audit_entry = AuditLogEntry(
user_id=user_id,
action=action,
timestamp=datetime.now(),
ip_address=ip_address,
user_agent=user_agent,
details=details
)
print(f"Audit Log: {audit_entry.dict()}")
++ b/app/auth/routes.py
@@
from .security import audit_logger
async def _log_audit_event(
user_id: str,
action: str,
request: Request = None,
details: Optional[Dict[str, Any]] = None
):
"""
Log an audit event
"""
- # This would typically save to a database
- # For demo purposes, we'll just print it
ip_address = request.client.host if request else None
user_agent = request.headers.get("user-agent") if request else None
-
- audit_entry = AuditLogEntry(
- user_id=user_id,
- action=action,
- timestamp=datetime.now(),
- ip_address=ip_address,
- user_agent=user_agent,
- details=details
- )
-
audit_logger.log_event(
user_id=user_id,
action=action,
ip_address=ip_address,
user_agent=user_agent,
details=details
)
🤖 Prompt for AI Agents
In app/auth/routes.py around lines 316 to 339, the helper `_log_audit_event`
duplicates audit logging instead of using the existing AuditLogger; update the
function to import and use AuditLogger from app.auth.security (or accept an
injected AuditLogger), construct the same audit payload (user_id, action,
timestamp, ip_address, user_agent, details) from the request, and call the
AuditLogger's appropriate log method rather than printing; ensure request can be
None, preserve timestamp handling, and remove the print-based AuditLogEntry
usage so all audit events go through the centralized AuditLogger.

Loading