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
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:
@@ -39,37 +39,37 @@ export class AuditLogRepository {
|
||||
let paramIndex = 1;
|
||||
|
||||
if (filters.search) {
|
||||
conditions.push(`action ILIKE $${paramIndex}`);
|
||||
conditions.push(`al.action ILIKE $${paramIndex}`);
|
||||
params.push(`%${this.escapeLikePattern(filters.search)}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (filters.category) {
|
||||
conditions.push(`category = $${paramIndex}`);
|
||||
conditions.push(`al.category = $${paramIndex}`);
|
||||
params.push(filters.category);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (filters.severity) {
|
||||
conditions.push(`severity = $${paramIndex}`);
|
||||
conditions.push(`al.severity = $${paramIndex}`);
|
||||
params.push(filters.severity);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (filters.userId) {
|
||||
conditions.push(`user_id = $${paramIndex}`);
|
||||
conditions.push(`al.user_id = $${paramIndex}`);
|
||||
params.push(filters.userId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (filters.startDate) {
|
||||
conditions.push(`created_at >= $${paramIndex}`);
|
||||
conditions.push(`al.created_at >= $${paramIndex}`);
|
||||
params.push(filters.startDate);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (filters.endDate) {
|
||||
conditions.push(`created_at <= $${paramIndex}`);
|
||||
conditions.push(`al.created_at <= $${paramIndex}`);
|
||||
params.push(filters.endDate);
|
||||
paramIndex++;
|
||||
}
|
||||
@@ -86,7 +86,8 @@ export class AuditLogRepository {
|
||||
const query = `
|
||||
INSERT INTO audit_logs (category, severity, user_id, action, resource_type, resource_id, details)
|
||||
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 {
|
||||
@@ -117,14 +118,17 @@ export class AuditLogRepository {
|
||||
const { whereClause, params, nextParamIndex } = this.buildWhereClause(filters);
|
||||
|
||||
// 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 = `
|
||||
SELECT id, category, severity, user_id, action, resource_type, resource_id, details, created_at
|
||||
FROM audit_logs
|
||||
SELECT al.id, al.category, al.severity, al.user_id, al.action,
|
||||
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}
|
||||
ORDER BY created_at DESC
|
||||
ORDER BY al.created_at DESC
|
||||
LIMIT $${nextParamIndex} OFFSET $${nextParamIndex + 1}
|
||||
`;
|
||||
|
||||
@@ -156,16 +160,19 @@ export class AuditLogRepository {
|
||||
const { whereClause, params } = this.buildWhereClause(filters);
|
||||
|
||||
// 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 totalCount = parseInt(countResult.rows[0].total, 10);
|
||||
const truncated = totalCount > MAX_EXPORT_RECORDS;
|
||||
|
||||
const query = `
|
||||
SELECT id, category, severity, user_id, action, resource_type, resource_id, details, created_at
|
||||
FROM audit_logs
|
||||
SELECT al.id, al.category, al.severity, al.user_id, al.action,
|
||||
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}
|
||||
ORDER BY created_at DESC
|
||||
ORDER BY al.created_at DESC
|
||||
LIMIT ${MAX_EXPORT_RECORDS}
|
||||
`;
|
||||
|
||||
@@ -222,6 +229,7 @@ export class AuditLogRepository {
|
||||
category: row.category as AuditLogEntry['category'],
|
||||
severity: row.severity as AuditLogEntry['severity'],
|
||||
userId: row.user_id as string | null,
|
||||
userEmail: (row.user_email as string | null) || null,
|
||||
action: row.action as string,
|
||||
resourceType: row.resource_type as string | null,
|
||||
resourceId: row.resource_id as string | null,
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface AuditLogEntry {
|
||||
category: AuditLogCategory;
|
||||
severity: AuditLogSeverity;
|
||||
userId: string | null;
|
||||
userEmail: string | null;
|
||||
action: string;
|
||||
resourceType: string | null;
|
||||
resourceId: string | null;
|
||||
|
||||
@@ -268,11 +268,15 @@ const AdminLogsMobileScreen: React.FC = () => {
|
||||
|
||||
{/* Metadata */}
|
||||
<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]">
|
||||
User: {log.userId.substring(0, 16)}...
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
{log.resourceType && log.resourceId && (
|
||||
<span className="truncate max-w-[150px]">
|
||||
{log.resourceType}: {log.resourceId.substring(0, 10)}...
|
||||
|
||||
@@ -388,6 +388,7 @@ export interface UnifiedAuditLog {
|
||||
category: AuditLogCategory;
|
||||
severity: AuditLogSeverity;
|
||||
userId: string | null;
|
||||
userEmail: string | null;
|
||||
action: string;
|
||||
resourceType: string | null;
|
||||
resourceId: string | null;
|
||||
|
||||
@@ -351,8 +351,12 @@ export const AdminLogsPage: React.FC = () => {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.userId ? (
|
||||
<Typography variant="body2" sx={{ maxWidth: 150, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{log.userEmail ? (
|
||||
<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)}...
|
||||
</Typography>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user