mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-23 02:26:20 +00:00
♻️ refactor: Share AUDIT_ACTIONS, tighten audit dep types, document route order
Exports a runtime AUDIT_ACTIONS array from packages/data-schemas alongside the AuditAction type so the Mongoose schema enum and the HTTP handler's whitelist consume one source of truth instead of duplicating the literal pair. Switches the grants handler's recordAuditEntry dep typing from a duplicated inline object literal returning Promise<unknown> to the published RecordAuditEntryInput type returning Promise<void>, and tightens the local emitAudit args to AuditAction. Replaces the local ParsedFilters interface in the audit-log handler with Omit<AuditLogFilters, 'offset' | 'limit'> to drop the duplicate definition. Drops the optional marker on AuditLog.createdAt. Mongoose always sets it at insert time, so callers treating it as nullable were guarding against a state the schema does not produce. Adds a comment on api/server/routes/admin/audit.js noting that /export.csv must precede /:id so a future contributor does not accidentally reorder them into a 404 trap.
This commit is contained in:
parent
2682d70dff
commit
9dd56f259d
7 changed files with 27 additions and 29 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AuditAction>(['grant_assigned', 'grant_removed']);
|
||||
const VALID_ACTIONS = new Set<AuditAction>(AUDIT_ACTIONS);
|
||||
const VALID_PRINCIPAL_TYPES = new Set<string>(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<AuditLogFilters, 'offset' | 'limit'>;
|
||||
|
||||
interface AuditLogQuery {
|
||||
search?: string;
|
||||
|
|
|
|||
|
|
@ -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<boolean>;
|
||||
/** 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<unknown>;
|
||||
recordAuditEntry?: (input: RecordAuditEntryInput) => Promise<void>;
|
||||
/** Resolves a User document's display name from its id (for audit actor name). */
|
||||
resolveUserName?: (userId: string | Types.ObjectId) => Promise<string | null>;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<IAuditLog>(
|
|||
{
|
||||
action: {
|
||||
type: String,
|
||||
enum: ['grant_assigned', 'grant_removed'],
|
||||
enum: [...AUDIT_ACTIONS],
|
||||
required: true,
|
||||
immutable: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue