diff --git a/api/server/routes/admin/audit.js b/api/server/routes/admin/audit.js index e94867631e..4e03023e1e 100644 --- a/api/server/routes/admin/audit.js +++ b/api/server/routes/admin/audit.js @@ -18,6 +18,7 @@ const handlers = createAdminAuditLogHandlers({ router.use(requireJwtAuth, requireAuditLogRead); router.get('/', handlers.listAuditLog); +/** `/export.csv` MUST precede `/:id` so it isn't matched as `{ id: 'export.csv' }`. */ router.get('/export.csv', handlers.exportAuditLogCsv); router.get('/:id', handlers.getAuditLogEntry); diff --git a/packages/api/src/admin/auditLog.ts b/packages/api/src/admin/auditLog.ts index 29fb39165d..2d151882c7 100644 --- a/packages/api/src/admin/auditLog.ts +++ b/packages/api/src/admin/auditLog.ts @@ -1,5 +1,5 @@ import { PrincipalType } from 'librechat-data-provider'; -import { logger, MAX_AUDIT_LOG_LIMIT } from '@librechat/data-schemas'; +import { AUDIT_ACTIONS, logger, MAX_AUDIT_LOG_LIMIT } from '@librechat/data-schemas'; import type { AdminAuditLogEntry, AuditAction, @@ -12,7 +12,7 @@ import type { ServerRequest } from '~/types/http'; const FORMULA_PREFIX = /^[=+\-@\t\r]/; const CSV_BOM = ''; -const VALID_ACTIONS = new Set(['grant_assigned', 'grant_removed']); +const VALID_ACTIONS = new Set(AUDIT_ACTIONS); const VALID_PRINCIPAL_TYPES = new Set(Object.values(PrincipalType)); /** @@ -154,16 +154,7 @@ function formatCsvRow(entry: AdminAuditLogEntry): string { return CSV_COLUMNS.map((c) => escapeCsvCell(String(entry[c.key] ?? ''))).join(','); } -interface ParsedFilters { - search?: string; - action?: AuditAction[]; - from?: Date; - to?: Date; - actorQuery?: string; - targetPrincipalType?: PrincipalType; - targetQuery?: string; - capability?: string; -} +type ParsedFilters = Omit; interface AuditLogQuery { search?: string; diff --git a/packages/api/src/admin/grants.ts b/packages/api/src/admin/grants.ts index 297efa620a..1209769ac4 100644 --- a/packages/api/src/admin/grants.ts +++ b/packages/api/src/admin/grants.ts @@ -5,7 +5,12 @@ import { SystemCapabilities, expandImplications, } from '@librechat/data-schemas'; -import type { ISystemGrant, SystemCapability } from '@librechat/data-schemas'; +import type { + AuditAction, + ISystemGrant, + RecordAuditEntryInput, + SystemCapability, +} from '@librechat/data-schemas'; import type { Response } from 'express'; import type { Types } from 'mongoose'; import type { ResolvedPrincipal } from '~/types/principal'; @@ -73,16 +78,7 @@ export interface AdminGrantsDeps { }) => ResolvedPrincipal[] | undefined; checkRoleExists?: (roleId: string) => Promise; /** Optional audit emission. Failure is logged but does not roll back the grant. */ - recordAuditEntry?: (input: { - action: 'grant_assigned' | 'grant_removed'; - actorId: string | Types.ObjectId; - actorName: string; - targetPrincipalType: PrincipalType; - targetPrincipalId: string | Types.ObjectId; - targetName: string; - capability: string; - tenantId?: string; - }) => Promise; + recordAuditEntry?: (input: RecordAuditEntryInput) => Promise; /** Resolves a User document's display name from its id (for audit actor name). */ resolveUserName?: (userId: string | Types.ObjectId) => Promise; } @@ -115,7 +111,7 @@ export function createAdminGrantsHandlers(deps: AdminGrantsDeps): { } = deps; async function emitAudit(args: { - action: 'grant_assigned' | 'grant_removed'; + action: AuditAction; caller: { userId: string; role: string; tenantId?: string }; principalType: PrincipalType; principalId: string; diff --git a/packages/data-schemas/src/index.ts b/packages/data-schemas/src/index.ts index e63dc02a15..5cdf5654ce 100644 --- a/packages/data-schemas/src/index.ts +++ b/packages/data-schemas/src/index.ts @@ -28,6 +28,7 @@ export { } from './methods'; export type * from './types'; export type * from './methods'; +export { AUDIT_ACTIONS } from './types/admin'; export { default as logger } from './config/winston'; export { default as meiliLogger } from './config/meiliLogger'; export { redactMessage } from './config/parsers'; diff --git a/packages/data-schemas/src/schema/auditLog.ts b/packages/data-schemas/src/schema/auditLog.ts index de310257e2..91d9b1aa5a 100644 --- a/packages/data-schemas/src/schema/auditLog.ts +++ b/packages/data-schemas/src/schema/auditLog.ts @@ -1,5 +1,6 @@ import { Schema } from 'mongoose'; import { PrincipalType } from 'librechat-data-provider'; +import { AUDIT_ACTIONS } from '~/types/admin'; import type { IAuditLog } from '~/types'; /** @@ -12,7 +13,7 @@ const auditLogSchema = new Schema( { action: { type: String, - enum: ['grant_assigned', 'grant_removed'], + enum: [...AUDIT_ACTIONS], required: true, immutable: true, }, diff --git a/packages/data-schemas/src/types/admin.ts b/packages/data-schemas/src/types/admin.ts index 755bb7a2e6..98f1841bfe 100644 --- a/packages/data-schemas/src/types/admin.ts +++ b/packages/data-schemas/src/types/admin.ts @@ -65,8 +65,13 @@ export type AdminConfigDeleteResponse = { success: boolean; }; +/** Single source of truth for the audit-action enum. Both the Mongoose schema + * enum and the HTTP handler's whitelist consume this constant so they cannot + * drift apart. */ +export const AUDIT_ACTIONS = ['grant_assigned', 'grant_removed'] as const; + /** Audit action types for grant changes. */ -export type AuditAction = 'grant_assigned' | 'grant_removed'; +export type AuditAction = (typeof AUDIT_ACTIONS)[number]; /** SystemGrant document as returned by the admin API. */ export type AdminSystemGrant = { diff --git a/packages/data-schemas/src/types/auditLog.ts b/packages/data-schemas/src/types/auditLog.ts index 271b9e77da..24f50828b5 100644 --- a/packages/data-schemas/src/types/auditLog.ts +++ b/packages/data-schemas/src/types/auditLog.ts @@ -3,9 +3,13 @@ import type { PrincipalType } from 'librechat-data-provider'; import type { AdminAuditLogEntry, AuditAction } from './admin'; /** - * AuditLog is an append-only collection — no `updatedAt`, no mutations. + * AuditLog is an append-only collection: no `updatedAt`, no mutations. * See `~/schema/auditLog` for the enforcement (immutable fields plus * pre-update / pre-delete / pre-save hooks). + * + * `createdAt` is always set by Mongoose at insert time, so this type + * intentionally declares it required. The `IAuditLog` intersection adds + * the document identity (`_id`). */ export type AuditLog = { action: AuditAction; @@ -19,13 +23,12 @@ export type AuditLog = { /** Absent = platform-operator audit; present = tenant-scoped audit */ tenantId?: string; /** Mongoose auto-managed via `timestamps: { createdAt: true, updatedAt: false }` */ - createdAt?: Date; + createdAt: Date; }; export type IAuditLog = AuditLog & Document & { _id: Types.ObjectId; - createdAt: Date; }; export interface RecordAuditEntryInput {