Record audit logs when the default parameters change (#22)

* Prepare for more endpoints that write logs

* Record audit logs when the default parameters change
This commit is contained in:
Canyon Robins 2025-05-16 17:06:38 -07:00 committed by GitHub
parent 2a8ab9dacb
commit 9b40f322df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 166 additions and 62 deletions

55
src/actions/apiUtils.ts Normal file
View file

@ -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',
};
}

View file

@ -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<typeof defaultParametersSchema>;
/**
* 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<ApiResponse> {
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');
}
}

View file

@ -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<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',
};
}
// 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<ApiResponse> {
@ -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<ApiResponse> {
@ -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<ApiResponse> {

View file

@ -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 (