feat: granular file access audit logging (viewed/opened/downloaded)
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

Replace single FILE_DOWNLOADED action with three granular actions:
- FILE_VIEWED: inline preview loaded in the UI
- FILE_OPENED: file opened in a new browser tab
- FILE_DOWNLOADED: explicit download button clicked

Add 'purpose' field to getDownloadUrl input (preview/open/download).
All client callers updated to pass the appropriate purpose.
Audit page updated with new filter options and color mappings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 18:18:28 +01:00
parent 503a375701
commit c8c26beed2
6 changed files with 32 additions and 15 deletions

View File

@@ -17,6 +17,8 @@ export const fileRouter = router({
objectKey: z.string(),
forDownload: z.boolean().optional(),
fileName: z.string().optional(),
/** Why the URL is being requested — drives audit log granularity. */
purpose: z.enum(['preview', 'open', 'download']).optional(),
})
)
.query(async ({ ctx, input }) => {
@@ -124,14 +126,25 @@ export const fileRouter = router({
})
}
// Only log actual downloads, not preview/view URL requests
if (input.forDownload) {
// Log file access with granular action based on purpose
const purpose = input.purpose ?? (input.forDownload ? 'download' : undefined)
if (purpose) {
const actionMap = {
preview: 'FILE_VIEWED',
open: 'FILE_OPENED',
download: 'FILE_DOWNLOADED',
} as const
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'FILE_DOWNLOADED',
action: actionMap[purpose],
entityType: 'ProjectFile',
detailsJson: { bucket: input.bucket, objectKey: input.objectKey, fileName: input.fileName },
detailsJson: {
bucket: input.bucket,
objectKey: input.objectKey,
fileName: input.fileName,
purpose,
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})