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;
|
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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)}...
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user