diff --git a/app/auth.py b/app/auth.py
index 8811df94..e2017623 100644
--- a/app/auth.py
+++ b/app/auth.py
@@ -15,6 +15,7 @@ from starlette.responses import RedirectResponse
from app.config import settings
from app.database import get_db
+from app.middleware.audit_log import get_client_ip
# Conditional imports: only used when multi_user_enabled=True. Imported here at
# module level (not inside auth()) so they don't incur repeated import overhead.
@@ -344,6 +345,13 @@ async def oauth_callback(request: Request, db: Session = Depends(get_db)):
# Log the successful authentication
logger.info("[SECURITY] OAUTH_LOGIN_SUCCESS user=%s admin=%s", user_data.get("email", "unknown"), is_admin)
+ _record_login_event(
+ db,
+ request,
+ user_data.get("email") or user_data.get("preferred_username") or "unknown",
+ success=True,
+ method="oauth",
+ )
# Redirect first-time users to onboarding
user_id = (
@@ -364,6 +372,48 @@ async def oauth_callback(request: Request, db: Session = Depends(get_db)):
return RedirectResponse(url=f"/login?error=Authentication+failed:+{str(e)}", status_code=status.HTTP_302_FOUND)
+def _record_login_event(
+ db: Session,
+ request: Request,
+ username: str,
+ *,
+ success: bool,
+ method: str = "local",
+ detail: str | None = None,
+) -> None:
+ """Write a login or login-failure audit event to the database.
+
+ Failures are silently swallowed so that an audit-service error never
+ prevents a legitimate login or surfaces an unrelated 500 error to the user.
+
+ Args:
+ db: Active database session.
+ request: The current HTTP request (used to extract the client IP).
+ username: The username that attempted authentication.
+ success: ``True`` for a successful login, ``False`` for a failure.
+ method: Authentication method, e.g. ``"local"`` or ``"oauth"``.
+ detail: Optional extra context for failures (e.g. ``"wrong_password"``).
+ """
+ try:
+ from app.utils.audit_service import record_event
+
+ action = "login" if success else "login.failure"
+ details: dict = {"method": method}
+ if detail:
+ details["reason"] = detail
+ record_event(
+ db,
+ action=action,
+ user=username,
+ resource_type="session",
+ ip_address=get_client_ip(request),
+ details=details,
+ severity="info" if success else "warning",
+ )
+ except Exception:
+ logger.debug("Failed to write login audit event for user=%s", username, exc_info=True)
+
+
async def auth(request: Request, db: Session = Depends(get_db)):
"""Handle local username/password authentication.
@@ -413,6 +463,7 @@ async def auth(request: Request, db: Session = Depends(get_db)):
username,
local_user.is_active,
)
+ _record_login_event(db, request, username, success=False, detail="account_not_verified")
return RedirectResponse(
url="/login?error=Please+verify+your+email+address+before+logging+in",
status_code=302,
@@ -425,10 +476,12 @@ async def auth(request: Request, db: Session = Depends(get_db)):
)
if not pw_ok:
logger.warning("[SECURITY] LOCAL_LOGIN_FAILURE reason=wrong_password user=%s", username)
+ _record_login_event(db, request, username, success=False, detail="wrong_password")
return RedirectResponse(url="/login?error=Invalid+username+or+password", status_code=302)
user_data = _build_session_user(local_user)
request.session["user"] = user_data
logger.info("[SECURITY] LOCAL_LOGIN_SUCCESS user=%s", local_user.email)
+ _record_login_event(db, request, local_user.email, success=True)
_ensure_user_profile(db, user_data, is_admin=bool(local_user.is_admin))
profile = db.query(_UserProfile).filter(_UserProfile.user_id == local_user.email).first()
if profile and not profile.onboarding_completed:
@@ -473,6 +526,7 @@ async def auth(request: Request, db: Session = Depends(get_db)):
}
request.session["user"] = admin_user_data
logger.info("[SECURITY] LOCAL_LOGIN_SUCCESS user=%s", username)
+ _record_login_event(db, request, username, success=True)
_ensure_user_profile(db, admin_user_data, is_admin=True)
redirect_url = request.session.pop("redirect_after_login", "/upload")
return RedirectResponse(url=redirect_url, status_code=302)
@@ -485,16 +539,30 @@ async def auth(request: Request, db: Session = Depends(get_db)):
admin_configured,
not username and not password,
)
+ _record_login_event(db, request, username or "anonymous", success=False, detail="invalid_credentials")
return RedirectResponse(url="/login?error=Invalid+username+or+password", status_code=302)
-async def logout(request: Request):
+async def logout(request: Request, db: Session = Depends(get_db)):
"""Handle user logout"""
user = request.session.get("user")
username = "unknown"
if isinstance(user, dict):
username = user.get("preferred_username") or user.get("email") or "unknown"
logger.info(f"[SECURITY] LOGOUT user={username}")
+ try:
+ from app.utils.audit_service import record_event
+
+ record_event(
+ db,
+ action="logout",
+ user=username,
+ resource_type="session",
+ ip_address=get_client_ip(request),
+ severity="info",
+ )
+ except Exception:
+ logger.debug("Failed to write logout audit event for user=%s", username, exc_info=True)
request.session.pop("user", None)
return RedirectResponse(url="/login?message=You+have+been+logged+out+successfully", status_code=302)
diff --git a/frontend/templates/base.html b/frontend/templates/base.html
index eef10e65..25d79498 100644
--- a/frontend/templates/base.html
+++ b/frontend/templates/base.html
@@ -177,9 +177,6 @@
Audit Logs
-
- Audit Logs
-
{{ _("nav.status") }}