mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
f228542f27
commit
4bfb3ef193
16 changed files with 312 additions and 460 deletions
|
|
@ -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
22
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
194
src/actions/providerWhitelist.ts
Normal file
194
src/actions/providerWhitelist.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue