fix: Display user email instead of Auth0 UID in audit logs (refs #10)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m40s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 37s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

- Add userEmail field to AuditLogEntry type in backend and frontend
- Update audit-log repository to LEFT JOIN with user_profiles table
- Update AdminLogsPage to show email with fallback to truncated userId
- Update AdminLogsMobileScreen with same display logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-11 12:30:57 -06:00
parent fbde51b8fd
commit 911b7c0e3a
5 changed files with 38 additions and 20 deletions

View File

@@ -39,37 +39,37 @@ export class AuditLogRepository {
let paramIndex = 1; let paramIndex = 1;
if (filters.search) { if (filters.search) {
conditions.push(`action ILIKE $${paramIndex}`); conditions.push(`al.action ILIKE $${paramIndex}`);
params.push(`%${this.escapeLikePattern(filters.search)}%`); params.push(`%${this.escapeLikePattern(filters.search)}%`);
paramIndex++; paramIndex++;
} }
if (filters.category) { if (filters.category) {
conditions.push(`category = $${paramIndex}`); conditions.push(`al.category = $${paramIndex}`);
params.push(filters.category); params.push(filters.category);
paramIndex++; paramIndex++;
} }
if (filters.severity) { if (filters.severity) {
conditions.push(`severity = $${paramIndex}`); conditions.push(`al.severity = $${paramIndex}`);
params.push(filters.severity); params.push(filters.severity);
paramIndex++; paramIndex++;
} }
if (filters.userId) { if (filters.userId) {
conditions.push(`user_id = $${paramIndex}`); conditions.push(`al.user_id = $${paramIndex}`);
params.push(filters.userId); params.push(filters.userId);
paramIndex++; paramIndex++;
} }
if (filters.startDate) { if (filters.startDate) {
conditions.push(`created_at >= $${paramIndex}`); conditions.push(`al.created_at >= $${paramIndex}`);
params.push(filters.startDate); params.push(filters.startDate);
paramIndex++; paramIndex++;
} }
if (filters.endDate) { if (filters.endDate) {
conditions.push(`created_at <= $${paramIndex}`); conditions.push(`al.created_at <= $${paramIndex}`);
params.push(filters.endDate); params.push(filters.endDate);
paramIndex++; paramIndex++;
} }
@@ -86,7 +86,8 @@ export class AuditLogRepository {
const query = ` const query = `
INSERT INTO audit_logs (category, severity, user_id, action, resource_type, resource_id, details) INSERT INTO audit_logs (category, severity, user_id, action, resource_type, resource_id, details)
VALUES ($1, $2, $3, $4, $5, $6, $7) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, category, severity, user_id, action, resource_type, resource_id, details, created_at RETURNING id, category, severity, user_id, action, resource_type, resource_id, details, created_at,
NULL::text as user_email
`; `;
try { try {
@@ -117,14 +118,17 @@ export class AuditLogRepository {
const { whereClause, params, nextParamIndex } = this.buildWhereClause(filters); const { whereClause, params, nextParamIndex } = this.buildWhereClause(filters);
// Count query // Count query
const countQuery = `SELECT COUNT(*) as total FROM audit_logs ${whereClause}`; const countQuery = `SELECT COUNT(*) as total FROM audit_logs al ${whereClause}`;
// Data query with pagination // Data query with pagination - LEFT JOIN to get user email
const dataQuery = ` const dataQuery = `
SELECT id, category, severity, user_id, action, resource_type, resource_id, details, created_at SELECT al.id, al.category, al.severity, al.user_id, al.action,
FROM audit_logs al.resource_type, al.resource_id, al.details, al.created_at,
up.email as user_email
FROM audit_logs al
LEFT JOIN user_profiles up ON al.user_id = up.auth0_sub
${whereClause} ${whereClause}
ORDER BY created_at DESC ORDER BY al.created_at DESC
LIMIT $${nextParamIndex} OFFSET $${nextParamIndex + 1} LIMIT $${nextParamIndex} OFFSET $${nextParamIndex + 1}
`; `;
@@ -156,16 +160,19 @@ export class AuditLogRepository {
const { whereClause, params } = this.buildWhereClause(filters); const { whereClause, params } = this.buildWhereClause(filters);
// First, count total matching records // First, count total matching records
const countQuery = `SELECT COUNT(*) as total FROM audit_logs ${whereClause}`; const countQuery = `SELECT COUNT(*) as total FROM audit_logs al ${whereClause}`;
const countResult = await this.pool.query(countQuery, params); const countResult = await this.pool.query(countQuery, params);
const totalCount = parseInt(countResult.rows[0].total, 10); const totalCount = parseInt(countResult.rows[0].total, 10);
const truncated = totalCount > MAX_EXPORT_RECORDS; const truncated = totalCount > MAX_EXPORT_RECORDS;
const query = ` const query = `
SELECT id, category, severity, user_id, action, resource_type, resource_id, details, created_at SELECT al.id, al.category, al.severity, al.user_id, al.action,
FROM audit_logs al.resource_type, al.resource_id, al.details, al.created_at,
up.email as user_email
FROM audit_logs al
LEFT JOIN user_profiles up ON al.user_id = up.auth0_sub
${whereClause} ${whereClause}
ORDER BY created_at DESC ORDER BY al.created_at DESC
LIMIT ${MAX_EXPORT_RECORDS} LIMIT ${MAX_EXPORT_RECORDS}
`; `;
@@ -222,6 +229,7 @@ export class AuditLogRepository {
category: row.category as AuditLogEntry['category'], category: row.category as AuditLogEntry['category'],
severity: row.severity as AuditLogEntry['severity'], severity: row.severity as AuditLogEntry['severity'],
userId: row.user_id as string | null, userId: row.user_id as string | null,
userEmail: (row.user_email as string | null) || null,
action: row.action as string, action: row.action as string,
resourceType: row.resource_type as string | null, resourceType: row.resource_type as string | null,
resourceId: row.resource_id as string | null, resourceId: row.resource_id as string | null,

View File

@@ -21,6 +21,7 @@ export interface AuditLogEntry {
category: AuditLogCategory; category: AuditLogCategory;
severity: AuditLogSeverity; severity: AuditLogSeverity;
userId: string | null; userId: string | null;
userEmail: string | null;
action: string; action: string;
resourceType: string | null; resourceType: string | null;
resourceId: string | null; resourceId: string | null;

View File

@@ -268,11 +268,15 @@ const AdminLogsMobileScreen: React.FC = () => {
{/* Metadata */} {/* Metadata */}
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-500"> <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-500">
{log.userId && ( {log.userEmail ? (
<span className="truncate max-w-[180px]">
User: {log.userEmail}
</span>
) : log.userId ? (
<span className="truncate max-w-[150px]"> <span className="truncate max-w-[150px]">
User: {log.userId.substring(0, 16)}... User: {log.userId.substring(0, 16)}...
</span> </span>
)} ) : null}
{log.resourceType && log.resourceId && ( {log.resourceType && log.resourceId && (
<span className="truncate max-w-[150px]"> <span className="truncate max-w-[150px]">
{log.resourceType}: {log.resourceId.substring(0, 10)}... {log.resourceType}: {log.resourceId.substring(0, 10)}...

View File

@@ -388,6 +388,7 @@ export interface UnifiedAuditLog {
category: AuditLogCategory; category: AuditLogCategory;
severity: AuditLogSeverity; severity: AuditLogSeverity;
userId: string | null; userId: string | null;
userEmail: string | null;
action: string; action: string;
resourceType: string | null; resourceType: string | null;
resourceId: string | null; resourceId: string | null;

View File

@@ -351,8 +351,12 @@ export const AdminLogsPage: React.FC = () => {
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
{log.userId ? ( {log.userEmail ? (
<Typography variant="body2" sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis' }}> <Typography variant="body2" sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis' }}>
{log.userEmail}
</Typography>
) : log.userId ? (
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis' }}>
{log.userId.substring(0, 20)}... {log.userId.substring(0, 20)}...
</Typography> </Typography>
) : ( ) : (