mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Make Audit logs write to and read from the DB (not vaporware) (#19)
* Add database schema for audit logs * update audit log code to read from the DB rather than mocks * use UUID not text for id * backend endpoint for writing audit logs * add stubs for provider whitelist update * wip on sending audit logs * remove oldValue field * remove references to oldValue * more * change toast * move querying to the backend * fix fetch * base URL * use a server-side function rather than endpoint for audit log creation * remove old_value * remove route for querying audit logs * don't take over date parsing * simplify, review feedback
This commit is contained in:
parent
502425f739
commit
f228542f27
19 changed files with 819 additions and 288 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
out: './migrations',
|
||||
out: './src/db/migrations',
|
||||
schema: './src/db/schema.ts',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: { url: process.env.DATABASE_URL! },
|
||||
|
|
|
|||
64
src/actions/auditLogs.ts
Normal file
64
src/actions/auditLogs.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
'use server';
|
||||
|
||||
import { eq, gte, and, desc } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/db';
|
||||
import { auditLogs } from '@/db/schema';
|
||||
import { logger } from '@/lib/server/logger';
|
||||
import { AuditLogType } from '@/types/auditLogs';
|
||||
|
||||
/**
|
||||
* getAuditLogs
|
||||
*
|
||||
* Server action to retrieve audit logs for an organization
|
||||
*/
|
||||
export const getAuditLogs = async ({
|
||||
orgId,
|
||||
limit = 100,
|
||||
nRecentDays,
|
||||
}: {
|
||||
orgId?: string | null;
|
||||
limit?: number;
|
||||
nRecentDays?: number;
|
||||
}): Promise<AuditLogType[]> => {
|
||||
if (!orgId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
if (isNaN(limit) || limit <= 0) {
|
||||
throw new Error('Limit must be a positive number');
|
||||
}
|
||||
if (nRecentDays === undefined) {
|
||||
return await db
|
||||
.select()
|
||||
.from(auditLogs)
|
||||
.where(eq(auditLogs.organizationId, orgId))
|
||||
.orderBy(desc(auditLogs.createdAt))
|
||||
.limit(limit);
|
||||
} else {
|
||||
if (isNaN(nRecentDays) || nRecentDays <= 0) {
|
||||
throw new Error('nRecentDays must be a positive number');
|
||||
}
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - nRecentDays);
|
||||
return await db
|
||||
.select()
|
||||
.from(auditLogs)
|
||||
.where(
|
||||
and(
|
||||
eq(auditLogs.organizationId, orgId),
|
||||
gte(auditLogs.createdAt, cutoff),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(auditLogs.createdAt))
|
||||
.limit(limit);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
event: 'audit_log_fetch_error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useOrganization } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -12,18 +14,28 @@ import {
|
|||
import { TitleBar } from '@/components/dashboard/TitleBar';
|
||||
import { AuditLogDetails } from '@/components/dashboard/AuditLogDetails';
|
||||
import { AuditLogEntry } from '@/components/dashboard/AuditLogEntry';
|
||||
import type { AuditLog } from '@/components/dashboard/mockAuditLogs';
|
||||
import { getFilteredLogs } from '@/components/dashboard/mockAuditLogs';
|
||||
import type { AuditLogType } from '@/types/auditLogs';
|
||||
import { getAuditLogs } from '@/actions/auditLogs';
|
||||
|
||||
type TimePeriod = '7' | '30' | '90';
|
||||
|
||||
const AuditLogsPage = () => {
|
||||
const [timePeriod, setTimePeriod] = useState<TimePeriod>('7');
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
|
||||
const { organization } = useOrganization();
|
||||
|
||||
const logs = getFilteredLogs(Number(timePeriod));
|
||||
const { data: logs = [], isLoading } = useQuery({
|
||||
queryKey: ['auditLogs', organization?.id, Number(timePeriod)],
|
||||
queryFn: async () =>
|
||||
await getAuditLogs({
|
||||
orgId: organization?.id,
|
||||
limit: 20,
|
||||
nRecentDays: Number(timePeriod),
|
||||
}),
|
||||
enabled: !!organization?.id,
|
||||
});
|
||||
|
||||
const handleLogClick = (log: AuditLog) => setSelectedLog(log);
|
||||
const handleLogClick = (log: AuditLogType) => setSelectedLog(log);
|
||||
const handleCloseDrawer = () => setSelectedLog(null);
|
||||
|
||||
return (
|
||||
|
|
@ -63,8 +75,12 @@ const AuditLogsPage = () => {
|
|||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{logs.length > 0 ? (
|
||||
logs.map((log: AuditLog) => (
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
) : logs.length > 0 ? (
|
||||
logs.map((log: AuditLogType) => (
|
||||
<AuditLogEntry key={log.id} log={log} onClick={handleLogClick} />
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
92
src/app/api/provider-whitelist/allow-all-providers/route.ts
Normal file
92
src/app/api/provider-whitelist/allow-all-providers/route.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AuditLogTargetType } from '@/types/auditLogs';
|
||||
import { logger } from '@/lib/server/logger';
|
||||
import { createAuditLog } from '@/lib/server/auditLogs';
|
||||
|
||||
// Allow all providers update schema
|
||||
const allowAllProvidersSchema = z.object({
|
||||
allowAllProviders: z.boolean(),
|
||||
policyVersion: z.number().int().positive(),
|
||||
});
|
||||
|
||||
export type AllowAllProvidersRequest = z.infer<typeof allowAllProvidersSchema>;
|
||||
export type AllowAllProvidersResponse = {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
// Note: I didn't really put any thought into the API as this is just stubs for the audit logs to work.
|
||||
// Consider merging this with the other provider-whitelist endpoints.
|
||||
// Please refactor!
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
): Promise<NextResponse<AllowAllProvidersResponse>> {
|
||||
try {
|
||||
const { userId, orgId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized: User required' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
if (!orgId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized: Organization required' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const result = allowAllProvidersSchema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
logger.warn({
|
||||
event: 'provider_whitelist_allow_all_providers_validation_failed',
|
||||
errors: result.error.format(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid request data' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const data = result.data;
|
||||
// TODO: persist data to the database
|
||||
|
||||
const auditLogData = {
|
||||
userId,
|
||||
organizationId: orgId,
|
||||
targetType: AuditLogTargetType.PROVIDER_WHITELIST,
|
||||
targetId: 'allow-all-providers',
|
||||
newValue: { allowAllProviders: data.allowAllProviders },
|
||||
description: `${data.allowAllProviders ? 'Enabled' : 'Disabled'} all providers`,
|
||||
};
|
||||
await createAuditLog(auditLogData);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Allow all providers setting updated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
event: 'provider_whitelist_allow_all_providers_update_error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'An unexpected error occurred',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
95
src/app/api/provider-whitelist/models/route.ts
Normal file
95
src/app/api/provider-whitelist/models/route.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AuditLogTargetType } from '@/types/auditLogs';
|
||||
import { logger } from '@/lib/server/logger';
|
||||
import { createAuditLog } from '@/lib/server/auditLogs';
|
||||
|
||||
// Model toggle update schema
|
||||
const modelToggleSchema = z.object({
|
||||
providerId: z.string().min(1, 'Provider ID is required'),
|
||||
modelId: z.string().min(1, 'Model ID is required'),
|
||||
enabled: z.boolean(),
|
||||
policyVersion: z.number().int().positive(),
|
||||
});
|
||||
|
||||
export type ModelToggleRequest = z.infer<typeof modelToggleSchema>;
|
||||
export type ModelToggleResponse = {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
// Note: I didn't really put any thought into the API as this is just stubs for the audit logs to work.
|
||||
// Consider merging this with the other provider-whitelist endpoints.
|
||||
// Please refactor!
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
): Promise<NextResponse<ModelToggleResponse>> {
|
||||
try {
|
||||
const { userId, orgId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized: User required' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
if (!orgId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized: Organization required' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const result = modelToggleSchema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
logger.warn({
|
||||
event: 'model_toggle_validation_failed',
|
||||
errors: result.error.format(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid request data' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const data = result.data;
|
||||
// TODO: persist data to the database
|
||||
|
||||
const auditLogData = {
|
||||
userId,
|
||||
organizationId: orgId,
|
||||
targetType: AuditLogTargetType.PROVIDER_WHITELIST,
|
||||
targetId: `${data.providerId}:${data.modelId}`,
|
||||
newValue: { enabled: data.enabled },
|
||||
description: `${data.enabled ? 'Enabled' : 'Disabled'} model ${data.modelId} for provider ${data.providerId}`,
|
||||
};
|
||||
await createAuditLog(auditLogData);
|
||||
|
||||
// Return success response
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Model status updated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
event: 'model_toggle_update_error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'An unexpected error occurred',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
93
src/app/api/provider-whitelist/providers/route.ts
Normal file
93
src/app/api/provider-whitelist/providers/route.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AuditLogTargetType } from '@/types/auditLogs';
|
||||
import { logger } from '@/lib/server/logger';
|
||||
import { createAuditLog } from '@/lib/server/auditLogs';
|
||||
|
||||
// Provider toggle update schema
|
||||
const providerToggleSchema = z.object({
|
||||
providerId: z.string().min(1, 'Provider ID is required'),
|
||||
enabled: z.boolean(),
|
||||
policyVersion: z.number().int().positive(),
|
||||
});
|
||||
|
||||
export type ProviderToggleRequest = z.infer<typeof providerToggleSchema>;
|
||||
export type ProviderToggleResponse = {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
// Note: I didn't really put any thought into the API as this is just stubs for the audit logs to work.
|
||||
// Consider merging this with the other provider-whitelist endpoints.
|
||||
// Please refactor!
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
): Promise<NextResponse<ProviderToggleResponse>> {
|
||||
try {
|
||||
const { userId, orgId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized: User required' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
if (!orgId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized: Organization required' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const result = providerToggleSchema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
logger.warn({
|
||||
event: 'provider_toggle_validation_failed',
|
||||
errors: result.error.format(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid request data' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const data = result.data;
|
||||
// TODO: persist data to the database
|
||||
|
||||
const auditLogData = {
|
||||
userId,
|
||||
organizationId: orgId,
|
||||
targetType: AuditLogTargetType.PROVIDER_WHITELIST,
|
||||
targetId: data.providerId,
|
||||
newValue: { enabled: data.enabled },
|
||||
description: `${data.enabled ? 'Enabled' : 'Disabled'} provider ${data.providerId}`,
|
||||
};
|
||||
await createAuditLog(auditLogData);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Provider status updated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
event: 'provider_toggle_update_error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'An unexpected error occurred',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import { useOrganization } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
Drawer,
|
||||
|
|
@ -12,14 +14,21 @@ import {
|
|||
|
||||
import { AuditLogDetails } from './AuditLogDetails';
|
||||
import { AuditLogEntry } from './AuditLogEntry';
|
||||
import type { AuditLog } from './mockAuditLogs';
|
||||
import { mockAuditLogs } from './mockAuditLogs';
|
||||
import type { AuditLogType } from '@/types/auditLogs';
|
||||
import { getAuditLogs } from '@/actions/auditLogs';
|
||||
|
||||
export function AuditLogCard() {
|
||||
const logs = mockAuditLogs.slice(0, 5);
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
|
||||
const { organization } = useOrganization();
|
||||
|
||||
const handleLogClick = (log: AuditLog) => setSelectedLog(log);
|
||||
const { data: logs = [], isLoading } = useQuery({
|
||||
queryKey: ['auditLogs', organization?.id, 5],
|
||||
queryFn: async () =>
|
||||
await getAuditLogs({ orgId: organization?.id, limit: 5 }),
|
||||
enabled: !!organization?.id,
|
||||
});
|
||||
|
||||
const handleLogClick = (log: AuditLogType) => setSelectedLog(log);
|
||||
const handleCloseDrawer = () => setSelectedLog(null);
|
||||
|
||||
return (
|
||||
|
|
@ -33,8 +42,12 @@ export function AuditLogCard() {
|
|||
|
||||
{/* Log entries */}
|
||||
<div className="space-y-1">
|
||||
{logs.length > 0 ? (
|
||||
logs.map((log: AuditLog) => (
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
) : logs.length > 0 ? (
|
||||
logs.map((log: AuditLogType) => (
|
||||
<AuditLogEntry key={log.id} log={log} onClick={handleLogClick} />
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
|
|||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
import type { AuditLog } from './mockAuditLogs';
|
||||
import type { AuditLogType } from '@/types/auditLogs';
|
||||
|
||||
type AuditLogDetailsProps = {
|
||||
log: AuditLog;
|
||||
log: AuditLogType;
|
||||
};
|
||||
|
||||
const formatValue = (value: unknown): React.ReactNode => {
|
||||
|
|
@ -48,19 +48,19 @@ export function AuditLogDetails({ log }: AuditLogDetailsProps) {
|
|||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
||||
<Calendar className="size-4" />
|
||||
<span>{log.timestamp.toLocaleDateString()}</span>
|
||||
<span>{log.createdAt.toLocaleDateString()}</span>
|
||||
<Clock className="ml-2 size-4" />
|
||||
<span>{log.timestamp.toLocaleTimeString()}</span>
|
||||
<span>{log.createdAt.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
||||
<User className="size-4" />
|
||||
<span>{log.user}</span>
|
||||
<span>{log.userId}</span>
|
||||
</div>
|
||||
|
||||
{log.path && (
|
||||
{log.targetId && (
|
||||
<Link
|
||||
href={log.path}
|
||||
href={log.targetId}
|
||||
className="mt-2 inline-flex items-center text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
View in settings
|
||||
|
|
@ -76,19 +76,10 @@ export function AuditLogDetails({ log }: AuditLogDetailsProps) {
|
|||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium uppercase text-muted-foreground">
|
||||
Before
|
||||
New Value
|
||||
</h4>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
{formatValue(log.details.before)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium uppercase text-muted-foreground">
|
||||
After
|
||||
</h4>
|
||||
<div className="rounded-md bg-muted p-3">
|
||||
{formatValue(log.details.after)}
|
||||
{formatValue(log.newValue)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,21 +5,21 @@ import React from 'react';
|
|||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import type { AuditLog, AuditLogType } from './mockAuditLogs';
|
||||
import { getFormattedTime } from './mockAuditLogs';
|
||||
import { getFormattedTime } from '../../lib/dateUtils';
|
||||
import { type AuditLogType, AuditLogTargetType } from '@/types/auditLogs';
|
||||
|
||||
type AuditLogEntryProps = {
|
||||
log: AuditLog;
|
||||
onClick: (log: AuditLog) => void;
|
||||
log: AuditLogType;
|
||||
onClick: (log: AuditLogType) => void;
|
||||
};
|
||||
|
||||
const getIconByType = (type: AuditLogType) => {
|
||||
const getIconByType = (type: AuditLogTargetType) => {
|
||||
switch (type) {
|
||||
case 'provider_whitelist':
|
||||
case AuditLogTargetType.PROVIDER_WHITELIST:
|
||||
return <Settings className="size-4 text-purple-500" />;
|
||||
case 'default_parameters':
|
||||
case AuditLogTargetType.DEFAULT_PARAMETERS:
|
||||
return <Sliders className="size-4 text-green-500" />;
|
||||
case 'member_change':
|
||||
case AuditLogTargetType.MEMBER_CHANGE:
|
||||
return <Users className="size-4 text-amber-500" />;
|
||||
default:
|
||||
return <Settings className="size-4 text-gray-500" />;
|
||||
|
|
@ -36,71 +36,13 @@ export function AuditLogEntry({ log, onClick }: AuditLogEntryProps) {
|
|||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 shrink-0">{getIconByType(log.type)}</div>
|
||||
<div className="mt-0.5 shrink-0">{getIconByType(log.targetType)}</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm text-foreground">{log.description}</p>
|
||||
|
||||
{/* Summary of changes */}
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{log.type === 'provider_whitelist' && (
|
||||
<span>
|
||||
{(() => {
|
||||
const before = log.details.before as string[];
|
||||
const after = log.details.after as string[];
|
||||
if (Array.isArray(before) && Array.isArray(after)) {
|
||||
if (after.length > before.length) {
|
||||
const added = after.filter((p) => !before.includes(p));
|
||||
return `Added: ${added.join(', ')}`;
|
||||
} else {
|
||||
const removed = before.filter((p) => !after.includes(p));
|
||||
return `Removed: ${removed.join(', ')}`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
})()}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{log.type === 'default_parameters' && (
|
||||
<span>
|
||||
{(() => {
|
||||
const before = log.details.before as Record<string, unknown>;
|
||||
const after = log.details.after as Record<string, unknown>;
|
||||
if (
|
||||
before &&
|
||||
after &&
|
||||
typeof before === 'object' &&
|
||||
typeof after === 'object'
|
||||
) {
|
||||
return Object.entries(after)
|
||||
.filter(([key, val]) => before[key] !== val)
|
||||
.map(([key, val]) => `${key}: ${before[key]} → ${val}`)
|
||||
.join(', ');
|
||||
}
|
||||
return '';
|
||||
})()}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{log.type === 'member_change' && (
|
||||
<span>
|
||||
{(() => {
|
||||
if (!log.details.before) {
|
||||
return 'New member added';
|
||||
}
|
||||
if (!log.details.after) {
|
||||
return 'Member removed';
|
||||
}
|
||||
return 'Role updated';
|
||||
})()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">{log.user}</p>
|
||||
<p className="text-xs text-muted-foreground">{log.userId}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{getFormattedTime(log.timestamp)}
|
||||
{getFormattedTime(log.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useTranslations } from 'next-intl';
|
|||
import { useState } from 'react';
|
||||
|
||||
import { Badge, Checkbox, Label } from '@/components/ui';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const initialProviders = [
|
||||
{
|
||||
|
|
@ -53,23 +54,134 @@ const ProviderWhitelistPage = () => {
|
|||
const [providers, setProviders] = useState(initialProviders);
|
||||
const [policyVersion] = useState(1);
|
||||
const [allowAllProviders, setAllowAllProviders] = useState(true);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
// API functions to send provider whitelist data to backend
|
||||
// Note: I'm not sure that it actually makes sense to have three separate endpoints here,
|
||||
// and I didn't really put any thought into their API as they're just stubs for the audit logs to work.
|
||||
// Please refactor!
|
||||
const updateAllowAllProvidersAPI = async (allowAllProviders: boolean) => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
'/api/provider-whitelist/allow-all-providers',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
allowAllProviders,
|
||||
policyVersion,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
|
||||
console.log('Allow all providers setting updated successfully', {
|
||||
allowAllProviders,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update global settings:', error);
|
||||
toast.error(
|
||||
'Failed to update allow all providers setting. Please try again.',
|
||||
);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateProviderAPI = async (providerId: string, enabled: boolean) => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await fetch('/api/provider-whitelist/providers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
providerId,
|
||||
enabled,
|
||||
policyVersion,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
|
||||
console.log('Provider status updated successfully', {
|
||||
providerId,
|
||||
enabled,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update provider status:', error);
|
||||
toast.error('Failed to update provider status. Please try again.');
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateModelAPI = async (
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
enabled: boolean,
|
||||
) => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await fetch('/api/provider-whitelist/models', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
providerId,
|
||||
modelId,
|
||||
enabled,
|
||||
policyVersion,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
|
||||
console.log('Model status updated successfully', {
|
||||
providerId,
|
||||
modelId,
|
||||
enabled,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update model status:', error);
|
||||
toast.error('Failed to update model status. Please try again.');
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAllowAllProviders = () => {
|
||||
const newAllowAllProviders = !allowAllProviders;
|
||||
setAllowAllProviders(newAllowAllProviders);
|
||||
|
||||
let updatedProviders = providers;
|
||||
if (newAllowAllProviders) {
|
||||
setProviders(
|
||||
providers.map((provider) => ({
|
||||
...provider,
|
||||
// Enable all providers and their models
|
||||
updatedProviders = providers.map((provider) => ({
|
||||
...provider,
|
||||
enabled: true,
|
||||
models: provider.models.map((model) => ({
|
||||
...model,
|
||||
enabled: true,
|
||||
models: provider.models.map((model) => ({
|
||||
...model,
|
||||
enabled: true,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
}));
|
||||
setProviders(updatedProviders);
|
||||
}
|
||||
|
||||
// Call API to update backend with the changed allow all providers setting
|
||||
updateAllowAllProvidersAPI(newAllowAllProviders);
|
||||
};
|
||||
|
||||
const toggleProvider = (providerId: string) => {
|
||||
|
|
@ -77,22 +189,31 @@ const ProviderWhitelistPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
setProviders(
|
||||
providers.map((provider) => {
|
||||
if (provider.id === providerId) {
|
||||
const newEnabled = !provider.enabled;
|
||||
return {
|
||||
...provider,
|
||||
enabled: newEnabled,
|
||||
models: provider.models.map((model) => ({
|
||||
...model,
|
||||
enabled: newEnabled ? model.enabled : false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return provider;
|
||||
}),
|
||||
);
|
||||
// Find the provider to get its current state
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
if (!provider) return;
|
||||
|
||||
const newEnabled = !provider.enabled;
|
||||
|
||||
const updatedProviders = providers.map((provider) => {
|
||||
if (provider.id === providerId) {
|
||||
return {
|
||||
...provider,
|
||||
enabled: newEnabled,
|
||||
// If provider is disabled, disable all its models
|
||||
models: provider.models.map((model) => ({
|
||||
...model,
|
||||
enabled: newEnabled ? model.enabled : false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return provider;
|
||||
});
|
||||
|
||||
setProviders(updatedProviders);
|
||||
|
||||
// Call API to update backend with the changed provider status
|
||||
updateProviderAPI(providerId, newEnabled);
|
||||
};
|
||||
|
||||
const toggleModel = (providerId: string, modelId: string) => {
|
||||
|
|
@ -100,22 +221,34 @@ const ProviderWhitelistPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
setProviders(
|
||||
providers.map((provider) => {
|
||||
if (provider.id === providerId) {
|
||||
return {
|
||||
...provider,
|
||||
models: provider.models.map((model) => {
|
||||
if (model.id === modelId) {
|
||||
return { ...model, enabled: !model.enabled };
|
||||
}
|
||||
return model;
|
||||
}),
|
||||
};
|
||||
}
|
||||
return provider;
|
||||
}),
|
||||
);
|
||||
// Find the model to get its current state
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
if (!provider) return;
|
||||
|
||||
const model = provider.models.find((m) => m.id === modelId);
|
||||
if (!model) return;
|
||||
|
||||
const newEnabled = !model.enabled;
|
||||
|
||||
const updatedProviders = providers.map((provider) => {
|
||||
if (provider.id === providerId) {
|
||||
return {
|
||||
...provider,
|
||||
models: provider.models.map((model) => {
|
||||
if (model.id === modelId) {
|
||||
return { ...model, enabled: newEnabled };
|
||||
}
|
||||
return model;
|
||||
}),
|
||||
};
|
||||
}
|
||||
return provider;
|
||||
});
|
||||
|
||||
setProviders(updatedProviders);
|
||||
|
||||
// Call API to update backend with the changed model status
|
||||
updateModelAPI(providerId, modelId, newEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -195,7 +328,9 @@ const ProviderWhitelistPage = () => {
|
|||
{`Policy v${policyVersion}`}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Changes will be pushed to SSE stream within 30 seconds
|
||||
{isUpdating
|
||||
? 'Updating provider whitelist...'
|
||||
: 'Changes will be pushed to SSE stream within 30 seconds'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
export type AuditLogType =
|
||||
| 'provider_whitelist'
|
||||
| 'default_parameters'
|
||||
| 'member_change';
|
||||
|
||||
export type AuditLogDetails = {
|
||||
before: unknown;
|
||||
after: unknown;
|
||||
};
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
type: AuditLogType;
|
||||
description: string;
|
||||
timestamp: Date;
|
||||
user: string;
|
||||
details: AuditLogDetails;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export const mockAuditLogs: AuditLog[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'provider_whitelist',
|
||||
description: 'Added OpenAI to provider whitelist',
|
||||
timestamp: new Date(2025, 4, 10, 14, 30),
|
||||
user: 'John Doe',
|
||||
details: {
|
||||
before: ['Anthropic', 'Cohere'],
|
||||
after: ['Anthropic', 'Cohere', 'OpenAI'],
|
||||
},
|
||||
path: '/dashboard/organization-profile/provider-whitelist',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'default_parameters',
|
||||
description: 'Updated default temperature parameter',
|
||||
timestamp: new Date(2025, 4, 9, 11, 15),
|
||||
user: 'Jane Smith',
|
||||
details: {
|
||||
before: { temperature: 0.7 },
|
||||
after: { temperature: 0.9 },
|
||||
},
|
||||
path: '/dashboard/organization-profile/default-parameters',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'member_change',
|
||||
description: 'Changed role for Alex Johnson from Member to Admin',
|
||||
timestamp: new Date(2025, 4, 8, 9, 45),
|
||||
user: 'Sarah Williams',
|
||||
details: {
|
||||
before: { role: 'Member' },
|
||||
after: { role: 'Admin' },
|
||||
},
|
||||
path: '/dashboard/organization-profile/organization-members',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'provider_whitelist',
|
||||
description: 'Removed Claude from provider whitelist',
|
||||
timestamp: new Date(2025, 4, 7, 16, 20),
|
||||
user: 'Michael Brown',
|
||||
details: {
|
||||
before: ['OpenAI', 'Claude', 'Cohere'],
|
||||
after: ['OpenAI', 'Cohere'],
|
||||
},
|
||||
path: '/dashboard/organization-profile/provider-whitelist',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'default_parameters',
|
||||
description: 'Updated max tokens parameter',
|
||||
timestamp: new Date(2025, 4, 6, 13, 10),
|
||||
user: 'Emily Davis',
|
||||
details: {
|
||||
before: { max_tokens: 1000 },
|
||||
after: { max_tokens: 2000 },
|
||||
},
|
||||
path: '/dashboard/organization-profile/default-parameters',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'member_change',
|
||||
description: 'Added new member David Wilson',
|
||||
timestamp: new Date(2025, 4, 5, 10, 30),
|
||||
user: 'John Doe',
|
||||
details: {
|
||||
before: null,
|
||||
after: {
|
||||
name: 'David Wilson',
|
||||
email: 'david@example.com',
|
||||
role: 'Member',
|
||||
},
|
||||
},
|
||||
path: '/dashboard/organization-profile/organization-members',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'member_change',
|
||||
description: 'Removed member Lisa Taylor',
|
||||
timestamp: new Date(2025, 4, 4, 15, 45),
|
||||
user: 'Sarah Williams',
|
||||
details: {
|
||||
before: {
|
||||
name: 'Lisa Taylor',
|
||||
email: 'lisa@example.com',
|
||||
role: 'Member',
|
||||
},
|
||||
after: null,
|
||||
},
|
||||
path: '/dashboard/organization-profile/organization-members',
|
||||
},
|
||||
];
|
||||
|
||||
export const getFilteredLogs = (days: number): AuditLog[] => {
|
||||
const now = new Date();
|
||||
const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
||||
|
||||
return mockAuditLogs.filter((log) => log.timestamp >= cutoff);
|
||||
};
|
||||
|
||||
export const getFormattedTime = (date: Date): string => {
|
||||
const now = new Date();
|
||||
const diffInHours = Math.floor(
|
||||
(now.getTime() - date.getTime()) / (1000 * 60 * 60),
|
||||
);
|
||||
|
||||
if (diffInHours < 24) {
|
||||
return diffInHours === 0
|
||||
? 'Just now'
|
||||
: diffInHours === 1
|
||||
? '1 hour ago'
|
||||
: `${diffInHours} hours ago`;
|
||||
}
|
||||
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
|
||||
if (diffInDays < 7) {
|
||||
return diffInDays === 1 ? 'Yesterday' : `${diffInDays} days ago`;
|
||||
}
|
||||
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
10
src/db/migrations/0000_daffy_psylocke.sql
Normal file
10
src/db/migrations/0000_daffy_psylocke.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
CREATE TABLE "audit_logs" (
|
||||
"id" uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
|
||||
"user_id" text NOT NULL,
|
||||
"organization_id" text NOT NULL,
|
||||
"target_type" integer NOT NULL,
|
||||
"target_id" text NOT NULL,
|
||||
"new_value" jsonb NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"description" text NOT NULL
|
||||
);
|
||||
81
src/db/migrations/meta/0000_snapshot.json
Normal file
81
src/db/migrations/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"id": "6600a193-62ff-4475-9533-eb1dafc7339e",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.audit_logs": {
|
||||
"name": "audit_logs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"target_type": {
|
||||
"name": "target_type",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"target_id": {
|
||||
"name": "target_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"new_value": {
|
||||
"name": "new_value",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
src/db/migrations/meta/_journal.json
Normal file
13
src/db/migrations/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1747347845063,
|
||||
"tag": "0000_daffy_psylocke",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1 +1,22 @@
|
|||
// TODO
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
integer,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
|
||||
// AuditLogType
|
||||
export const auditLogs = pgTable('audit_logs', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id').notNull(),
|
||||
organizationId: text('organization_id').notNull(),
|
||||
targetType: integer('target_type').notNull(), // AuditLogTargetType
|
||||
targetId: text('target_id').notNull(),
|
||||
newValue: jsonb('new_value').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
description: text('description').notNull(),
|
||||
});
|
||||
|
|
|
|||
23
src/lib/dateUtils.ts
Normal file
23
src/lib/dateUtils.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Format time for display - this is a client-side utility function
|
||||
export function getFormattedTime(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffInHours = Math.floor(
|
||||
(now.getTime() - date.getTime()) / (1000 * 60 * 60),
|
||||
);
|
||||
|
||||
if (diffInHours < 24) {
|
||||
return diffInHours === 0
|
||||
? 'Just now'
|
||||
: diffInHours === 1
|
||||
? '1 hour ago'
|
||||
: `${diffInHours} hours ago`;
|
||||
}
|
||||
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
|
||||
if (diffInDays < 7) {
|
||||
return diffInDays === 1 ? 'Yesterday' : `${diffInDays} days ago`;
|
||||
}
|
||||
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
50
src/lib/server/auditLogs.ts
Normal file
50
src/lib/server/auditLogs.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { db } from '@/db';
|
||||
import { auditLogs } from '@/db/schema';
|
||||
import { auditLogSchema } from '@/types/auditLogs';
|
||||
import { logger } from '@/lib/server/logger';
|
||||
import type { z } from 'zod';
|
||||
|
||||
export type AuditLogCreateRequest = z.infer<typeof auditLogSchema>;
|
||||
|
||||
/**
|
||||
* Server-side function to create audit logs in the database
|
||||
* @param request The audit log data to create
|
||||
* @returns Object containing success status and created log ID
|
||||
*/
|
||||
export async function createAuditLog(request: AuditLogCreateRequest): Promise<{
|
||||
success: boolean;
|
||||
error?: string | Record<string, unknown>;
|
||||
}> {
|
||||
try {
|
||||
await db.insert(auditLogs).values({
|
||||
userId: request.userId,
|
||||
organizationId: request.organizationId,
|
||||
targetType: request.targetType,
|
||||
targetId: request.targetId,
|
||||
newValue: request.newValue,
|
||||
description: request.description,
|
||||
// id and createdAt will be set automatically by the database
|
||||
});
|
||||
|
||||
logger.info({
|
||||
event: 'audit_log_created',
|
||||
userId: request.userId,
|
||||
organizationId: request.organizationId,
|
||||
targetType: request.targetType,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
event: 'audit_log_creation_error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'An unexpected error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
36
src/types/auditLogs.ts
Normal file
36
src/types/auditLogs.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Client-safe type definitions for audit logs
|
||||
* These types mirror the database schema but are safe to import in client components
|
||||
*/
|
||||
|
||||
export enum AuditLogTargetType {
|
||||
PROVIDER_WHITELIST = 1,
|
||||
DEFAULT_PARAMETERS = 2,
|
||||
MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected
|
||||
}
|
||||
|
||||
export interface AuditLogType {
|
||||
id: string;
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
targetType: AuditLogTargetType;
|
||||
targetId: string;
|
||||
newValue: unknown; // JSONB in the database
|
||||
createdAt: Date;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Validation schema for audit log creation
|
||||
export const auditLogSchema = z.object({
|
||||
userId: z.string().min(1, 'User ID is required'),
|
||||
// You should only create audit logs for the authenticated organization
|
||||
organizationId: z.string().min(1, 'Organization ID is required'),
|
||||
targetType: z.nativeEnum(AuditLogTargetType, {
|
||||
errorMap: () => ({ message: 'Target type must be a valid enum value' }),
|
||||
}),
|
||||
targetId: z.string().min(1, 'Target ID is required'),
|
||||
newValue: z.any().refine((val) => val !== undefined, 'New value is required'),
|
||||
description: z.string().min(1, 'Description is required'),
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue