Add user names to audit log displays (#58)

Include the email in the detail view.
This commit is contained in:
John Richmond 2025-05-31 19:41:45 -07:00 committed by GitHub
parent 65e38dee4d
commit db2d8d3d50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 49 additions and 38 deletions

View file

@ -5,7 +5,7 @@ import { eq, gte, and, desc } from 'drizzle-orm';
import { logger } from '@/lib/server';
import {
type DatabaseOrTransaction,
type AuditLog,
type AuditLogWithUser,
type CreateAuditLog,
client as db,
auditLogs,
@ -19,7 +19,7 @@ export const getAuditLogs = async ({
orgId?: string | null;
limit?: number;
timePeriod?: number;
}): Promise<AuditLog[]> => {
}): Promise<AuditLogWithUser[]> => {
if (!orgId) {
return [];
}
@ -30,12 +30,14 @@ export const getAuditLogs = async ({
}
if (timePeriod === undefined) {
return await db
.select()
.from(auditLogs)
.where(eq(auditLogs.orgId, orgId))
.orderBy(desc(auditLogs.createdAt))
.limit(limit);
return await db.query.auditLogs.findMany({
where: eq(auditLogs.orgId, orgId),
with: {
user: true,
},
orderBy: desc(auditLogs.createdAt),
limit: limit,
});
} else {
if (isNaN(timePeriod) || timePeriod <= 0) {
throw new Error('timePeriod must be a positive number');
@ -44,14 +46,17 @@ export const getAuditLogs = async ({
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - timePeriod);
return await db
.select()
.from(auditLogs)
.where(
and(eq(auditLogs.orgId, orgId), gte(auditLogs.createdAt, cutoff)),
)
.orderBy(desc(auditLogs.createdAt))
.limit(limit);
return await db.query.auditLogs.findMany({
where: and(
eq(auditLogs.orgId, orgId),
gte(auditLogs.createdAt, cutoff),
),
with: {
user: true,
},
orderBy: desc(auditLogs.createdAt),
limit: limit,
});
}
} catch (error) {
logger.error({

View file

@ -5,7 +5,7 @@ import { useAuth } from '@clerk/nextjs';
import { useQuery } from '@tanstack/react-query';
import { type TimePeriod, timePeriods } from '@/types';
import type { AuditLog } from '@/db';
import type { AuditLogWithUser } from '@/db';
import { getAuditLogs } from '@/actions/auditLogs';
import {
Button,
@ -29,7 +29,7 @@ export const AuditLogs = () => {
enabled: !!orgId,
});
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const [selectedLog, setSelectedLog] = useState<AuditLogWithUser | null>(null);
return (
<>
@ -61,11 +61,11 @@ export const AuditLogs = () => {
<Skeleton className="h-[64px] w-full" />
</>
) : logs.length > 0 ? (
logs.map((log: AuditLog) => (
logs.map((log: AuditLogWithUser) => (
<AuditLogEntry
key={log.id}
log={log}
onClick={(log: AuditLog) => setSelectedLog(log)}
onClick={(log: AuditLogWithUser) => setSelectedLog(log)}
/>
))
) : (

View file

@ -7,7 +7,7 @@ import { useAuth } from '@clerk/nextjs';
import { useQuery } from '@tanstack/react-query';
import { ArrowRightIcon } from 'lucide-react';
import type { AuditLog } from '@/db';
import type { AuditLogWithUser } from '@/db';
import { getAuditLogs } from '@/actions/auditLogs';
import {
Card,
@ -33,7 +33,7 @@ export function AuditLogCard() {
enabled: !!orgId,
});
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const [selectedLog, setSelectedLog] = useState<AuditLogWithUser | null>(null);
const path = usePathname();
return (
@ -70,11 +70,11 @@ export function AuditLogCard() {
<Skeleton className="h-[64px] w-full" />
</>
) : logs.length > 0 ? (
logs.map((log: AuditLog) => (
logs.map((log: AuditLogWithUser) => (
<AuditLogEntry
key={log.id}
log={log}
onClick={(log: AuditLog) => setSelectedLog(log)}
onClick={(log: AuditLogWithUser) => setSelectedLog(log)}
/>
))
) : (

View file

@ -1,9 +1,9 @@
import Link from 'next/link';
import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
import type { AuditLog } from '@/db';
import type { AuditLogWithUser } from '@/db';
export const AuditLogDetails = ({ log }: { log: AuditLog }) => (
export const AuditLogDetails = ({ log }: { log: AuditLogWithUser }) => (
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
@ -14,7 +14,9 @@ export const AuditLogDetails = ({ log }: { log: AuditLog }) => (
</div>
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<User className="size-4" />
<span>{log.userId}</span>
<span>
{log.user.name} ({log.user.email})
</span>
</div>
{log.targetId && (
<Link

View file

@ -1,4 +1,4 @@
import type { AuditLog } from '@/db';
import type { AuditLogWithUser } from '@/db';
import {
Drawer,
DrawerContent,
@ -8,7 +8,7 @@ import {
import { AuditLogDetails } from '@/components/audit-logs';
type AuditLogDrawerProps = {
selectedLog: AuditLog | null;
selectedLog: AuditLogWithUser | null;
onClose: () => void;
};

View file

@ -1,11 +1,11 @@
import { formatDistance } from 'date-fns';
import { Settings, Sliders, Users } from 'lucide-react';
import { type AuditLog, AuditLogTargetType } from '@/db';
import { type AuditLogWithUser, AuditLogTargetType } from '@/db';
type AuditLogEntryProps = {
log: AuditLog;
onClick: (log: AuditLog) => void;
log: AuditLogWithUser;
onClick: (log: AuditLogWithUser) => void;
};
export const AuditLogEntry = ({ log, onClick }: AuditLogEntryProps) => (
@ -18,7 +18,7 @@ export const AuditLogEntry = ({ log, onClick }: AuditLogEntryProps) => (
<div className="flex flex-row justify-between items-center gap-2 flex-1">
<div className="flex flex-col gap-1">
<p className="text-sm text-foreground">{log.description}</p>
<p className="text-xs text-muted-foreground">{log.userId}</p>
<p className="text-xs text-muted-foreground">{log.user.name}</p>
</div>
<p className="text-xs text-muted-foreground hidden sm:block">
{formatDistance(log.createdAt, new Date(), { addSuffix: true })}

View file

@ -2,10 +2,13 @@ import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { Env } from '@/lib/server';
import * as schema from './schema';
const pgClient = postgres(Env.DATABASE_URL, { prepare: false });
let testDb: ReturnType<typeof drizzle> | undefined = undefined;
const client = drizzle({ client: pgClient, schema });
let testDb: typeof client | undefined = undefined;
if (process.env.NODE_ENV === 'test') {
if (
@ -15,12 +18,9 @@ if (process.env.NODE_ENV === 'test') {
throw new Error('DATABASE_URL is not a test database');
}
testDb = drizzle({ client: pgClient });
testDb = client;
}
const client =
process.env.NODE_ENV === 'test' ? testDb! : drizzle({ client: pgClient });
const disconnect = async () => {
await pgClient.end();
};

View file

@ -36,3 +36,7 @@ export type CreateOrgSettings = Omit<
export type AuditLog = typeof auditLogs.$inferSelect;
export type CreateAuditLog = Omit<typeof auditLogs.$inferInsert, Generated>;
export type AuditLogWithUser = AuditLog & {
user: User;
};