Fix audit log target for deleted users + create user 500 error

1. Audit log: COALESCE target_username with detail JSON fallback so
   deleted users still show their username in the target column
   (target_user_id is SET NULL by FK cascade, but detail JSON
   preserves the username).

2. Create/get user: add exclude={"active_sessions"} to model_dump()
   calls — UserListItem defaults active_sessions=0, so model_dump()
   includes it, then the explicit active_sessions=N keyword argument
   causes a duplicate keyword TypeError. DB commit already happened,
   so the user exists but the response was a 500.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-27 19:44:10 +08:00
parent 48e15fa677
commit c3654dc069

View File

@ -16,6 +16,7 @@ from typing import Optional
import sqlalchemy as sa import sqlalchemy as sa
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
@ -58,6 +59,22 @@ router = APIRouter(
) )
# ---------------------------------------------------------------------------
# Audit log helper — resolve target username even for deleted users
# ---------------------------------------------------------------------------
def _target_username_col(target_alias, audit_model):
"""
COALESCE: prefer the live username from the users table,
fall back to the username stored in the audit detail JSON
(survives user deletion since audit_log.target_user_id SET NULL).
"""
return sa.func.coalesce(
target_alias.username,
sa.cast(audit_model.detail, JSONB)["username"].as_string(),
).label("target_username")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Session revocation helper (used in multiple endpoints) # Session revocation helper (used in multiple endpoints)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -152,7 +169,7 @@ async def get_user(
active_sessions = session_result.scalar_one() active_sessions = session_result.scalar_one()
return UserDetailResponse( return UserDetailResponse(
**UserListItem.model_validate(user).model_dump(), **UserListItem.model_validate(user).model_dump(exclude={"active_sessions"}),
active_sessions=active_sessions, active_sessions=active_sessions,
) )
@ -197,7 +214,7 @@ async def create_user(
await db.commit() await db.commit()
return UserDetailResponse( return UserDetailResponse(
**UserListItem.model_validate(new_user).model_dump(), **UserListItem.model_validate(new_user).model_dump(exclude={"active_sessions"}),
active_sessions=0, active_sessions=0,
) )
@ -668,7 +685,7 @@ async def admin_dashboard(
sa.select( sa.select(
AuditLog, AuditLog,
actor_user.username.label("actor_username"), actor_user.username.label("actor_username"),
target_user.username.label("target_username"), _target_username_col(target_user, AuditLog),
) )
.outerjoin(actor_user, AuditLog.actor_user_id == actor_user.id) .outerjoin(actor_user, AuditLog.actor_user_id == actor_user.id)
.outerjoin(target_user, AuditLog.target_user_id == target_user.id) .outerjoin(target_user, AuditLog.target_user_id == target_user.id)
@ -722,7 +739,7 @@ async def get_audit_log(
sa.select( sa.select(
AuditLog, AuditLog,
actor_user.username.label("actor_username"), actor_user.username.label("actor_username"),
target_user.username.label("target_username"), _target_username_col(target_user, AuditLog),
) )
.outerjoin(actor_user, AuditLog.actor_user_id == actor_user.id) .outerjoin(actor_user, AuditLog.actor_user_id == actor_user.id)
.outerjoin(target_user, AuditLog.target_user_id == target_user.id) .outerjoin(target_user, AuditLog.target_user_id == target_user.id)