Improve audit log implementation (#20)

* Replace date utils with library

* use drizzle-zod

* use drizzle types directly

* use server actions rather than routes

* slight refactor

* UI rollback on API failure

* slight refactor to reduce verbosity

* clean up
This commit is contained in:
Canyon Robins 2025-05-16 16:12:24 -07:00 committed by GitHub
parent f228542f27
commit 4bfb3ef193
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 312 additions and 460 deletions

View file

@ -50,7 +50,9 @@
"@tanstack/react-table": "^8.20.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
"lucide-react": "^0.509.0",
"next": "^15.3.2",
"next-intl": "^4.1.0",

22
pnpm-lock.yaml generated
View file

@ -86,9 +86,15 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
date-fns:
specifier: ^4.1.0
version: 4.1.0
drizzle-orm:
specifier: ^0.43.1
version: 0.43.1(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.0)(@types/pg@8.15.1)(pg@8.15.6)
drizzle-zod:
specifier: ^0.7.1
version: 0.7.1(drizzle-orm@0.43.1(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.0)(@types/pg@8.15.1)(pg@8.15.6))(zod@3.24.4)
lucide-react:
specifier: ^0.509.0
version: 0.509.0(react@19.1.0)
@ -4743,6 +4749,9 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
@ -5001,6 +5010,12 @@ packages:
sqlite3:
optional: true
drizzle-zod@0.7.1:
resolution: {integrity: sha512-nZzALOdz44/AL2U005UlmMqaQ1qe5JfanvLujiTHiiT8+vZJTBFhj3pY4Vk+L6UWyKFfNmLhk602Hn4kCTynKQ==}
peerDependencies:
drizzle-orm: '>=0.36.0'
zod: '>=3.0.0'
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@ -13925,6 +13940,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
date-fns@4.1.0: {}
dateformat@4.6.3: {}
debounce@1.2.1: {}
@ -14083,6 +14100,11 @@ snapshots:
'@types/pg': 8.15.1
pg: 8.15.6
drizzle-zod@0.7.1(drizzle-orm@0.43.1(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.0)(@types/pg@8.15.1)(pg@8.15.6))(zod@3.24.4):
dependencies:
drizzle-orm: 0.43.1(@electric-sql/pglite@0.3.0)(@libsql/client-wasm@0.15.5)(@opentelemetry/api@1.9.0)(@types/pg@8.15.1)(pg@8.15.6)
zod: 3.24.4
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2

View file

@ -5,7 +5,7 @@ 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';
import { AuditLogType } from '@/db/schema';
/**
* getAuditLogs

View file

@ -0,0 +1,194 @@
'use server';
import { auth } from '@clerk/nextjs/server';
import { z } from 'zod';
import { AuditLogTargetType } from '@/db/schema';
import { logger } from '@/lib/server/logger';
import { createAuditLog } from '@/lib/server/auditLogs';
type ApiResponse = {
success: boolean;
message?: string;
error?: string;
};
const allowAllProvidersSchema = z.object({
allowAllProviders: z.boolean(),
policyVersion: z.number().int().positive(),
});
const providerToggleSchema = z.object({
providerId: z.string().min(1, 'Provider ID is required'),
enabled: z.boolean(),
policyVersion: z.number().int().positive(),
});
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(),
});
type AllowAllProvidersRequest = z.infer<typeof allowAllProvidersSchema>;
type ProviderToggleRequest = z.infer<typeof providerToggleSchema>;
type ModelToggleRequest = z.infer<typeof modelToggleSchema>;
/**
* Validates user authentication and organization membership
* @returns User and organization IDs if authenticated, or error response if not
*/
async function validateAuth(): Promise<
{ userId: string; orgId: string } | ApiResponse
> {
const { userId, orgId } = await auth();
if (!userId) {
return {
success: false,
error: 'Unauthorized: User required',
};
}
if (!orgId) {
return {
success: false,
error: 'Unauthorized: Organization required',
};
}
return { userId, orgId };
}
function isAuthSuccess(
result: { userId: string; orgId: string } | ApiResponse,
): result is { userId: string; orgId: string } {
return !('error' in result);
}
/**
* Generic error handler for all operations
* @param error The caught error
* @param eventPrefix Prefix for logging events
* @returns Error response
*/
function handleError(error: unknown, eventPrefix: string): ApiResponse {
logger.error({
event: `${eventPrefix}_update_error`,
error: error instanceof Error ? error.message : 'Unknown error',
});
return {
success: false,
error:
error instanceof Error ? error.message : 'An unexpected error occurred',
};
}
export async function updateAllowAllProviders(
data: AllowAllProvidersRequest,
): Promise<ApiResponse> {
try {
const authResult = await validateAuth();
if (!isAuthSuccess(authResult)) return authResult;
const { userId, orgId } = authResult;
const result = allowAllProvidersSchema.safeParse(data);
if (!result.success) {
return {
success: false,
error: 'Invalid request data',
};
}
const validatedData = result.data;
// TODO: persist data to the database
await createAuditLog({
userId,
organizationId: orgId,
targetType: AuditLogTargetType.PROVIDER_WHITELIST,
targetId: 'allow-all-providers',
newValue: { allowAllProviders: validatedData.allowAllProviders },
description: `${validatedData.allowAllProviders ? 'Enabled' : 'Disabled'} all providers`,
});
return {
success: true,
message: 'Allow all providers setting updated successfully',
};
} catch (error) {
return handleError(error, 'provider_whitelist_allow_all_providers');
}
}
export async function updateProviderStatus(
data: ProviderToggleRequest,
): Promise<ApiResponse> {
try {
const authResult = await validateAuth();
if (!isAuthSuccess(authResult)) return authResult;
const { userId, orgId } = authResult;
const result = providerToggleSchema.safeParse(data);
if (!result.success) {
return {
success: false,
error: 'Invalid request data',
};
}
const validatedData = result.data;
// TODO: persist data to the database
await createAuditLog({
userId,
organizationId: orgId,
targetType: AuditLogTargetType.PROVIDER_WHITELIST,
targetId: validatedData.providerId,
newValue: { enabled: validatedData.enabled },
description: `${validatedData.enabled ? 'Enabled' : 'Disabled'} provider ${validatedData.providerId}`,
});
return {
success: true,
message: 'Provider status updated successfully',
};
} catch (error) {
return handleError(error, 'provider_toggle');
}
}
export async function updateModelStatus(
data: ModelToggleRequest,
): Promise<ApiResponse> {
try {
const authResult = await validateAuth();
if (!isAuthSuccess(authResult)) return authResult;
const { userId, orgId } = authResult;
const result = modelToggleSchema.safeParse(data);
if (!result.success) {
return {
success: false,
error: 'Invalid request data',
};
}
const validatedData = result.data;
// TODO: persist data to the database
await createAuditLog({
userId,
organizationId: orgId,
targetType: AuditLogTargetType.PROVIDER_WHITELIST,
targetId: `${validatedData.providerId}:${validatedData.modelId}`,
newValue: { enabled: validatedData.enabled },
description: `${validatedData.enabled ? 'Enabled' : 'Disabled'} model ${validatedData.modelId} for provider ${validatedData.providerId}`,
});
return {
success: true,
message: 'Model status updated successfully',
};
} catch (error) {
return handleError(error, 'model_toggle');
}
}

View file

@ -14,7 +14,7 @@ import {
import { TitleBar } from '@/components/dashboard/TitleBar';
import { AuditLogDetails } from '@/components/dashboard/AuditLogDetails';
import { AuditLogEntry } from '@/components/dashboard/AuditLogEntry';
import type { AuditLogType } from '@/types/auditLogs';
import type { AuditLogType } from '@/db/schema';
import { getAuditLogs } from '@/actions/auditLogs';
type TimePeriod = '7' | '30' | '90';

View file

@ -1,92 +0,0 @@
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 },
);
}
}

View file

@ -1,95 +0,0 @@
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 },
);
}
}

View file

@ -1,93 +0,0 @@
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 },
);
}
}

View file

@ -14,7 +14,7 @@ import {
import { AuditLogDetails } from './AuditLogDetails';
import { AuditLogEntry } from './AuditLogEntry';
import type { AuditLogType } from '@/types/auditLogs';
import type { AuditLogType } from '@/db/schema';
import { getAuditLogs } from '@/actions/auditLogs';
export function AuditLogCard() {

View file

@ -4,7 +4,7 @@ import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
import Link from 'next/link';
import React from 'react';
import type { AuditLogType } from '@/types/auditLogs';
import type { AuditLogType } from '@/db/schema';
type AuditLogDetailsProps = {
log: AuditLogType;

View file

@ -5,8 +5,8 @@ import React from 'react';
import { cn } from '@/lib/utils';
import { getFormattedTime } from '../../lib/dateUtils';
import { type AuditLogType, AuditLogTargetType } from '@/types/auditLogs';
import { formatDistance } from 'date-fns';
import { type AuditLogType, AuditLogTargetType } from '@/db/schema';
type AuditLogEntryProps = {
log: AuditLogType;
@ -42,7 +42,7 @@ export function AuditLogEntry({ log, onClick }: AuditLogEntryProps) {
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">{log.userId}</p>
<p className="text-xs text-muted-foreground">
{getFormattedTime(log.createdAt)}
{formatDistance(log.createdAt, new Date(), { addSuffix: true })}
</p>
</div>
</div>

View file

@ -3,6 +3,11 @@
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import {
updateAllowAllProviders,
updateModelStatus,
updateProviderStatus,
} from '@/actions/providerWhitelist';
import { Badge, Checkbox, Label } from '@/components/ui';
import { toast } from 'sonner';
@ -56,113 +61,16 @@ const ProviderWhitelistPage = () => {
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);
}
// Common error handling function for API calls
const handleApiError = (error: unknown, errorMessage: string) => {
console.error(errorMessage, error);
toast.error(errorMessage);
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 toggleAllowAllProviders = async () => {
const previousAllowAllProviders = allowAllProviders;
const previousProviders = [...providers];
const newAllowAllProviders = !allowAllProviders;
setAllowAllProviders(newAllowAllProviders);
@ -181,10 +89,28 @@ const ProviderWhitelistPage = () => {
}
// Call API to update backend with the changed allow all providers setting
updateAllowAllProvidersAPI(newAllowAllProviders);
setIsUpdating(true);
try {
const result = await updateAllowAllProviders({
allowAllProviders: newAllowAllProviders,
policyVersion,
});
if (!result.success) {
throw new Error(result.error || 'Failed to update setting');
}
} catch (error) {
setAllowAllProviders(previousAllowAllProviders);
setProviders(previousProviders);
handleApiError(
error,
'Failed to update allow all providers setting. Please try again.',
);
} finally {
setIsUpdating(false);
}
};
const toggleProvider = (providerId: string) => {
const toggleProvider = async (providerId: string) => {
if (allowAllProviders) {
return;
}
@ -193,6 +119,7 @@ const ProviderWhitelistPage = () => {
const provider = providers.find((p) => p.id === providerId);
if (!provider) return;
const previousProviders = [...providers];
const newEnabled = !provider.enabled;
const updatedProviders = providers.map((provider) => {
@ -213,10 +140,28 @@ const ProviderWhitelistPage = () => {
setProviders(updatedProviders);
// Call API to update backend with the changed provider status
updateProviderAPI(providerId, newEnabled);
setIsUpdating(true);
try {
const result = await updateProviderStatus({
providerId,
enabled: newEnabled,
policyVersion,
});
if (!result.success) {
throw new Error(result.error || 'Failed to update provider status');
}
} catch (error) {
setProviders(previousProviders);
handleApiError(
error,
'Failed to update provider status. Please try again.',
);
} finally {
setIsUpdating(false);
}
};
const toggleModel = (providerId: string, modelId: string) => {
const toggleModel = async (providerId: string, modelId: string) => {
if (allowAllProviders) {
return;
}
@ -228,6 +173,7 @@ const ProviderWhitelistPage = () => {
const model = provider.models.find((m) => m.id === modelId);
if (!model) return;
const previousProviders = [...providers];
const newEnabled = !model.enabled;
const updatedProviders = providers.map((provider) => {
@ -248,7 +194,23 @@ const ProviderWhitelistPage = () => {
setProviders(updatedProviders);
// Call API to update backend with the changed model status
updateModelAPI(providerId, modelId, newEnabled);
setIsUpdating(true);
try {
const result = await updateModelStatus({
providerId,
modelId,
enabled: newEnabled,
policyVersion,
});
if (!result.success) {
throw new Error(result.error || 'Failed to update model status');
}
} catch (error) {
setProviders(previousProviders);
handleApiError(error, 'Failed to update model status. Please try again.');
} finally {
setIsUpdating(false);
}
};
return (

View file

@ -7,7 +7,6 @@ import {
uuid,
} from 'drizzle-orm/pg-core';
// AuditLogType
export const auditLogs = pgTable('audit_logs', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').notNull(),
@ -20,3 +19,13 @@ export const auditLogs = pgTable('audit_logs', {
.notNull(),
description: text('description').notNull(),
});
export enum AuditLogTargetType {
PROVIDER_WHITELIST = 1,
DEFAULT_PARAMETERS = 2,
MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected
}
export type AuditLogType = Omit<typeof auditLogs.$inferSelect, 'targetType'> & {
targetType: AuditLogTargetType;
};

View file

@ -1,23 +0,0 @@
// 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();
}

View file

@ -1,15 +1,17 @@
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';
import { createInsertSchema } from 'drizzle-zod';
export type AuditLogCreateRequest = z.infer<typeof auditLogSchema>;
type AuditLogCreateRequest = z.infer<
ReturnType<typeof createInsertSchema<typeof auditLogs>>
>;
/**
* 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
* @returns Object containing success status or error
*/
export async function createAuditLog(request: AuditLogCreateRequest): Promise<{
success: boolean;

View file

@ -1,36 +0,0 @@
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'),
});