From 9b40f322dfcdd3c09b3a56ab4c82ba5749dd9a91 Mon Sep 17 00:00:00 2001 From: Canyon Robins Date: Fri, 16 May 2025 17:06:38 -0700 Subject: [PATCH] Record audit logs when the default parameters change (#22) * Prepare for more endpoints that write logs * Record audit logs when the default parameters change --- src/actions/apiUtils.ts | 55 +++++++++++++ src/actions/defaultParameters.ts | 82 +++++++++++++++++++ src/actions/providerWhitelist.ts | 67 +++------------ .../dashboard/DefaultParametersPage.tsx | 24 ++++-- 4 files changed, 166 insertions(+), 62 deletions(-) create mode 100644 src/actions/apiUtils.ts create mode 100644 src/actions/defaultParameters.ts diff --git a/src/actions/apiUtils.ts b/src/actions/apiUtils.ts new file mode 100644 index 0000000000..ea14074d4e --- /dev/null +++ b/src/actions/apiUtils.ts @@ -0,0 +1,55 @@ +import { auth } from '@clerk/nextjs/server'; +import { logger } from '@/lib/server/logger'; + +export type ApiResponse = { + success: boolean; + message?: string; + error?: string; +}; + +/** + * Validates user authentication and organization membership + * @returns User and organization IDs if authenticated, or error response if not + */ +export 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 }; +} + +export 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 + */ +export 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', + }; +} diff --git a/src/actions/defaultParameters.ts b/src/actions/defaultParameters.ts new file mode 100644 index 0000000000..40917c2111 --- /dev/null +++ b/src/actions/defaultParameters.ts @@ -0,0 +1,82 @@ +'use server'; + +import { z } from 'zod'; + +import { AuditLogTargetType } from '@/db/schema'; +import { createAuditLog } from '@/lib/server/auditLogs'; +import { + handleError, + isAuthSuccess, + validateAuth, + type ApiResponse, +} from './apiUtils'; + +const defaultParametersSchema = z.object({ + experimentalPowerSteering: z.boolean().optional(), + terminalOutputLimit: z.number().int().nonnegative().optional(), + compressProgressBar: z.boolean().optional(), + inheritEnvVars: z.boolean().optional(), + disableShellIntegration: z.boolean().optional(), + shellIntegrationTimeout: z.number().int().nonnegative().optional(), + commandDelay: z.number().int().nonnegative().optional(), + enablePowerShellCounter: z.boolean().optional(), + clearZshEol: z.boolean().optional(), + enableOhMyZsh: z.boolean().optional(), + enablePowerlevel10k: z.boolean().optional(), + openTabsLimit: z.number().int().nonnegative().optional(), + workspaceFilesLimit: z.number().int().nonnegative().optional(), + showRooignoreFiles: z.boolean().optional(), + fileReadThreshold: z.number().int().optional(), + alwaysReadEntireFile: z.boolean().optional(), + enableAutoCheckpoints: z.boolean().optional(), + useCustomTemperature: z.boolean().optional(), + temperature: z.number().nonnegative().optional(), + rateLimit: z.number().nonnegative().optional(), + enableEditingThroughDiffs: z.boolean().optional(), + matchPrecision: z.number().int().min(50).max(100).optional(), +}); + +type DefaultParametersRequest = z.infer; + +/** + * Updates default parameters and creates an audit log entry + * @param data The default parameters data to update + * @returns API response indicating success or failure + */ +export async function updateDefaultParameters( + data: DefaultParametersRequest, +): Promise { + try { + const authResult = await validateAuth(); + if (!isAuthSuccess(authResult)) return authResult; + const { userId, orgId } = authResult; + + const result = defaultParametersSchema.safeParse(data); + if (!result.success) { + return { + success: false, + error: 'Invalid request data', + }; + } + const validatedData = result.data; + + // TODO: persist data to the database + + // TODO: audit log description should contain more granular information on what changed + await createAuditLog({ + userId, + organizationId: orgId, + targetType: AuditLogTargetType.DEFAULT_PARAMETERS, + targetId: 'default-parameters', + newValue: validatedData, + description: 'Updated default parameters', + }); + + return { + success: true, + message: 'Default parameters updated successfully', + }; + } catch (error) { + return handleError(error, 'default_parameters'); + } +} diff --git a/src/actions/providerWhitelist.ts b/src/actions/providerWhitelist.ts index 97dc7e99cf..5d273e4072 100644 --- a/src/actions/providerWhitelist.ts +++ b/src/actions/providerWhitelist.ts @@ -1,17 +1,15 @@ '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; -}; +import { + handleError, + isAuthSuccess, + validateAuth, + type ApiResponse, +} from './apiUtils'; const allowAllProvidersSchema = z.object({ allowAllProviders: z.boolean(), @@ -35,53 +33,8 @@ 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', - }; -} - +// Note: I haven't thought much about this API; this is a temporary stub to get audit logs working. +// Feel free to refactor when implementing actual data writes. export async function updateAllowAllProviders( data: AllowAllProvidersRequest, ): Promise { @@ -119,6 +72,8 @@ export async function updateAllowAllProviders( } } +// Note: I haven't thought much about this API; this is a temporary stub to get audit logs working. +// Feel free to refactor when implementing actual data writes. export async function updateProviderStatus( data: ProviderToggleRequest, ): Promise { @@ -156,6 +111,8 @@ export async function updateProviderStatus( } } +// Note: I haven't thought much about this API; this is a temporary stub to get audit logs working. +// Feel free to refactor when implementing actual data writes. export async function updateModelStatus( data: ModelToggleRequest, ): Promise { diff --git a/src/components/dashboard/DefaultParametersPage.tsx b/src/components/dashboard/DefaultParametersPage.tsx index 0c7729d1a8..a1198afd66 100644 --- a/src/components/dashboard/DefaultParametersPage.tsx +++ b/src/components/dashboard/DefaultParametersPage.tsx @@ -7,6 +7,7 @@ import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; +import { updateDefaultParameters } from '@/actions/defaultParameters'; import { Button, Checkbox, @@ -77,17 +78,26 @@ const DefaultParametersPage = () => { }, }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const onSubmit = (_data: DefaultParamsFormValues) => { + const onSubmit = async (data: DefaultParamsFormValues) => { setIsSaving(true); - setTimeout(() => { - toast('Settings saved', { - description: 'Default parameters have been updated successfully.', + try { + const result = await updateDefaultParameters(data); + if (result.success) { + toast('Settings saved', { + description: 'Default parameters have been updated successfully.', + }); + } else { + throw new Error(result.error || 'An unexpected error occurred.'); + } + } catch (error) { + console.error('Failed to update default parameters:', error); + toast.error('Error saving settings', { + description: 'Failed to update default parameters. Please try again.', }); - + } finally { setIsSaving(false); - }, 1000); + } }; return (