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
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.asyncio import AsyncSession
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)
# ---------------------------------------------------------------------------
@ -152,7 +169,7 @@ async def get_user(
active_sessions = session_result.scalar_one()
return UserDetailResponse(
**UserListItem.model_validate(user).model_dump(),
**UserListItem.model_validate(user).model_dump(exclude={"active_sessions"}),
active_sessions=active_sessions,
)
@ -197,7 +214,7 @@ async def create_user(
await db.commit()
return UserDetailResponse(
**UserListItem.model_validate(new_user).model_dump(),
**UserListItem.model_validate(new_user).model_dump(exclude={"active_sessions"}),
active_sessions=0,
)
@ -668,7 +685,7 @@ async def admin_dashboard(
sa.select(
AuditLog,
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(target_user, AuditLog.target_user_id == target_user.id)
@ -722,7 +739,7 @@ async def get_audit_log(
sa.select(
AuditLog,
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(target_user, AuditLog.target_user_id == target_user.id)