feat: automatic mutation audit logging for all non-super-admin users
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

Implement withMutationAudit middleware in tRPC that automatically logs
every successful mutation for non-SUPER_ADMIN users. Captures procedure
path, sanitized input (passwords/tokens redacted), user role, IP, and
user agent. Applied to all procedure types except superAdminProcedure.

- Input sanitization: strips sensitive fields, truncates long strings
  (500 chars), limits array size (20 items), caps nesting depth (4)
- Entity ID auto-extraction from common input patterns (id, userId,
  projectId, roundId, etc.)
- Action names derived from procedure path (e.g., evaluation.submit
  becomes EVALUATION_SUBMIT)
- Audit page updated with new action types and entity types for
  filtering auto-generated entries
- Failures silently caught — audit logging never breaks operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 18:04:52 +01:00
parent 6c52e519e5
commit 79ac60dc1e
2 changed files with 218 additions and 32 deletions

View File

@@ -57,8 +57,9 @@ import { CsvExportDialog } from '@/components/shared/csv-export-dialog'
import { formatDate } from '@/lib/utils'
import { cn } from '@/lib/utils'
// Action type options
// Action type options (manual audit actions + auto-generated mutation audit actions)
const ACTION_TYPES = [
// Manual audit actions
'CREATE',
'UPDATE',
'DELETE',
@@ -88,12 +89,48 @@ const ACTION_TYPES = [
'APPLY_AI_SUGGESTIONS',
'APPLY_SUGGESTIONS',
'NOTIFY_JURORS_OF_ASSIGNMENTS',
'IMPERSONATION_START',
'IMPERSONATION_END',
// Auto-generated mutation audit actions (non-super-admin)
'EVALUATION_START',
'EVALUATION_SUBMIT',
'EVALUATION_AUTOSAVE',
'EVALUATION_DECLARE_COI',
'EVALUATION_ADD_COMMENT',
'APPLICANT_SAVE_SUBMISSION',
'APPLICANT_SAVE_FILE_METADATA',
'APPLICANT_DELETE_FILE',
'APPLICANT_REQUEST_MENTORING',
'APPLICANT_WITHDRAW_FROM_COMPETITION',
'APPLICANT_INVITE_TEAM_MEMBER',
'APPLICANT_REMOVE_TEAM_MEMBER',
'APPLICANT_SEND_MENTOR_MESSAGE',
'APPLICATION_SUBMIT',
'APPLICATION_SAVE_DRAFT',
'APPLICATION_SUBMIT_DRAFT',
'MENTOR_SEND_MESSAGE',
'MENTOR_CREATE_NOTE',
'MENTOR_DELETE_NOTE',
'MENTOR_COMPLETE_MILESTONE',
'LIVE_CAST_VOTE',
'LIVE_CAST_STAGE_VOTE',
'LIVE_VOTING_VOTE',
'LIVE_VOTING_CAST_AUDIENCE_VOTE',
'DELIBERATION_SUBMIT_VOTE',
'NOTIFICATION_MARK_AS_READ',
'NOTIFICATION_MARK_ALL_AS_READ',
'USER_UPDATE_PROFILE',
'USER_SET_PASSWORD',
'USER_CHANGE_PASSWORD',
'USER_COMPLETE_ONBOARDING',
'SPECIAL_AWARD_SUBMIT_VOTE',
]
// Entity type options
const ENTITY_TYPES = [
'User',
'Program',
'Competition',
'Round',
'Project',
'Assignment',
@@ -101,6 +138,21 @@ const ENTITY_TYPES = [
'EvaluationForm',
'ProjectFile',
'GracePeriod',
'Applicant',
'Application',
'Mentor',
'Live',
'LiveVoting',
'Deliberation',
'Notification',
'SpecialAward',
'File',
'Tag',
'Message',
'Settings',
'Ranking',
'Filtering',
'RoundEngine',
]
// Color map for action types
@@ -128,8 +180,35 @@ const actionColors: Record<string, 'default' | 'destructive' | 'secondary' | 'ou
APPLY_AI_SUGGESTIONS: 'default',
APPLY_SUGGESTIONS: 'default',
NOTIFY_JURORS_OF_ASSIGNMENTS: 'outline',
IMPERSONATION_START: 'destructive',
IMPERSONATION_END: 'secondary',
// Auto-generated mutation audit actions
EVALUATION_START: 'default',
EVALUATION_SUBMIT: 'default',
EVALUATION_AUTOSAVE: 'outline',
EVALUATION_DECLARE_COI: 'secondary',
EVALUATION_ADD_COMMENT: 'outline',
APPLICANT_SAVE_SUBMISSION: 'default',
APPLICANT_DELETE_FILE: 'destructive',
APPLICANT_WITHDRAW_FROM_COMPETITION: 'destructive',
APPLICANT_INVITE_TEAM_MEMBER: 'default',
APPLICANT_REMOVE_TEAM_MEMBER: 'destructive',
APPLICATION_SUBMIT: 'default',
MENTOR_SEND_MESSAGE: 'outline',
MENTOR_CREATE_NOTE: 'default',
MENTOR_DELETE_NOTE: 'destructive',
LIVE_CAST_VOTE: 'default',
LIVE_CAST_STAGE_VOTE: 'default',
LIVE_VOTING_CAST_AUDIENCE_VOTE: 'default',
DELIBERATION_SUBMIT_VOTE: 'default',
SPECIAL_AWARD_SUBMIT_VOTE: 'default',
USER_UPDATE_PROFILE: 'secondary',
USER_SET_PASSWORD: 'outline',
USER_CHANGE_PASSWORD: 'outline',
USER_COMPLETE_ONBOARDING: 'default',
}
export default function AuditLogPage() {
// Filter state
const [filters, setFilters] = useState({