From 4bfb3ef193b62077f41c26eeed09b1005b8a7fae Mon Sep 17 00:00:00 2001 From: Canyon Robins Date: Fri, 16 May 2025 16:12:24 -0700 Subject: [PATCH] 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 --- package.json | 2 + pnpm-lock.yaml | 22 ++ src/actions/auditLogs.ts | 2 +- src/actions/providerWhitelist.ts | 194 ++++++++++++++++++ .../dashboard/audit-logs/page.tsx | 2 +- .../allow-all-providers/route.ts | 92 --------- .../api/provider-whitelist/models/route.ts | 95 --------- .../api/provider-whitelist/providers/route.ts | 93 --------- src/components/dashboard/AuditLogCard.tsx | 2 +- src/components/dashboard/AuditLogDetails.tsx | 2 +- src/components/dashboard/AuditLogEntry.tsx | 6 +- .../dashboard/ProviderWhitelistPage.tsx | 182 +++++++--------- src/db/schema.ts | 11 +- src/lib/dateUtils.ts | 23 --- src/lib/server/auditLogs.ts | 8 +- src/types/auditLogs.ts | 36 ---- 16 files changed, 312 insertions(+), 460 deletions(-) create mode 100644 src/actions/providerWhitelist.ts delete mode 100644 src/app/api/provider-whitelist/allow-all-providers/route.ts delete mode 100644 src/app/api/provider-whitelist/models/route.ts delete mode 100644 src/app/api/provider-whitelist/providers/route.ts delete mode 100644 src/lib/dateUtils.ts delete mode 100644 src/types/auditLogs.ts diff --git a/package.json b/package.json index 2a809fc494..0b14ba5472 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2afbb10ab6..a17a38593e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/actions/auditLogs.ts b/src/actions/auditLogs.ts index 83db4a6465..765aeb30ba 100644 --- a/src/actions/auditLogs.ts +++ b/src/actions/auditLogs.ts @@ -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 diff --git a/src/actions/providerWhitelist.ts b/src/actions/providerWhitelist.ts new file mode 100644 index 0000000000..97dc7e99cf --- /dev/null +++ b/src/actions/providerWhitelist.ts @@ -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; +type ProviderToggleRequest = z.infer; +type ModelToggleRequest = z.infer; + +/** + * 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 { + 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 { + 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 { + 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'); + } +} diff --git a/src/app/(authenticated)/dashboard/audit-logs/page.tsx b/src/app/(authenticated)/dashboard/audit-logs/page.tsx index 23a324772e..8599f0e07b 100644 --- a/src/app/(authenticated)/dashboard/audit-logs/page.tsx +++ b/src/app/(authenticated)/dashboard/audit-logs/page.tsx @@ -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'; diff --git a/src/app/api/provider-whitelist/allow-all-providers/route.ts b/src/app/api/provider-whitelist/allow-all-providers/route.ts deleted file mode 100644 index 889d55fddf..0000000000 --- a/src/app/api/provider-whitelist/allow-all-providers/route.ts +++ /dev/null @@ -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; -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> { - 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 }, - ); - } -} diff --git a/src/app/api/provider-whitelist/models/route.ts b/src/app/api/provider-whitelist/models/route.ts deleted file mode 100644 index a943d7f803..0000000000 --- a/src/app/api/provider-whitelist/models/route.ts +++ /dev/null @@ -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; -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> { - 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 }, - ); - } -} diff --git a/src/app/api/provider-whitelist/providers/route.ts b/src/app/api/provider-whitelist/providers/route.ts deleted file mode 100644 index 356637a1b1..0000000000 --- a/src/app/api/provider-whitelist/providers/route.ts +++ /dev/null @@ -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; -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> { - 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 }, - ); - } -} diff --git a/src/components/dashboard/AuditLogCard.tsx b/src/components/dashboard/AuditLogCard.tsx index 199f247d2d..a2a89c55b4 100644 --- a/src/components/dashboard/AuditLogCard.tsx +++ b/src/components/dashboard/AuditLogCard.tsx @@ -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() { diff --git a/src/components/dashboard/AuditLogDetails.tsx b/src/components/dashboard/AuditLogDetails.tsx index af60ade4d3..c494dcd264 100644 --- a/src/components/dashboard/AuditLogDetails.tsx +++ b/src/components/dashboard/AuditLogDetails.tsx @@ -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; diff --git a/src/components/dashboard/AuditLogEntry.tsx b/src/components/dashboard/AuditLogEntry.tsx index 017ca5c877..2e7cf3034b 100644 --- a/src/components/dashboard/AuditLogEntry.tsx +++ b/src/components/dashboard/AuditLogEntry.tsx @@ -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) {

{log.userId}

- {getFormattedTime(log.createdAt)} + {formatDistance(log.createdAt, new Date(), { addSuffix: true })}

diff --git a/src/components/dashboard/ProviderWhitelistPage.tsx b/src/components/dashboard/ProviderWhitelistPage.tsx index e6f1b3c427..c1c5f2ee1f 100644 --- a/src/components/dashboard/ProviderWhitelistPage.tsx +++ b/src/components/dashboard/ProviderWhitelistPage.tsx @@ -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 ( diff --git a/src/db/schema.ts b/src/db/schema.ts index c35fd13a92..d86b56a98b 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -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 & { + targetType: AuditLogTargetType; +}; diff --git a/src/lib/dateUtils.ts b/src/lib/dateUtils.ts deleted file mode 100644 index a9ad825c70..0000000000 --- a/src/lib/dateUtils.ts +++ /dev/null @@ -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(); -} diff --git a/src/lib/server/auditLogs.ts b/src/lib/server/auditLogs.ts index 8e2f99fb84..cd888a1253 100644 --- a/src/lib/server/auditLogs.ts +++ b/src/lib/server/auditLogs.ts @@ -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; +type AuditLogCreateRequest = z.infer< + ReturnType> +>; /** * 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; diff --git a/src/types/auditLogs.ts b/src/types/auditLogs.ts deleted file mode 100644 index 09356f7f6e..0000000000 --- a/src/types/auditLogs.ts +++ /dev/null @@ -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'), -});