feat: Implement Comprehensive Audit Logging System#3721
feat: Implement Comprehensive Audit Logging System#3721suguslove10 wants to merge 11 commits intoDokploy:canaryfrom
Conversation
…stability enhancements
| try { | ||
| const result = await db | ||
| .delete(applications) | ||
| .where(eq(applications.applicationId, input.applicationId)) | ||
| .returning(); | ||
|
|
||
| await recordActivity({ | ||
| userId: ctx.user.id, | ||
| organizationId: ctx.session.activeOrganizationId, | ||
| action: "application.delete", | ||
| resourceType: "application", | ||
| resourceId: application.applicationId, | ||
| metadata: { name: application.name }, | ||
| }); | ||
|
|
||
| if (!IS_CLOUD) { | ||
| const queueJobs = await getJobsByApplicationId(input.applicationId); | ||
| for (const job of queueJobs) { | ||
| if (job.id) { | ||
| deploymentWorker.cancelJob(job.id, "User requested cancellation"); | ||
| if (!IS_CLOUD) { | ||
| const queueJobs = await getJobsByApplicationId(input.applicationId); | ||
| for (const job of queueJobs) { | ||
| if (job.id) { | ||
| deploymentWorker.cancelJob(job.id, "User requested cancellation"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const cleanupOperations = [ | ||
| async () => await deleteAllMiddlewares(application), | ||
| async () => await removeDeployments(application), | ||
| async () => | ||
| await removeDirectoryCode(application.appName, application.serverId), | ||
| async () => | ||
| await removeMonitoringDirectory( | ||
| application.appName, | ||
| application.serverId, | ||
| ), | ||
| async () => | ||
| await removeTraefikConfig(application.appName, application.serverId), | ||
| async () => | ||
| await removeService(application?.appName, application.serverId), | ||
| ]; | ||
|
|
||
| for (const operation of cleanupOperations) { | ||
| try { | ||
| await operation(); | ||
| } catch (_) {} | ||
| } | ||
| const cleanupOperations = [ | ||
| async () => await deleteAllMiddlewares(application), | ||
| async () => await removeDeployments(application), | ||
| async () => | ||
| await removeDirectoryCode( | ||
| application.appName, | ||
| application.serverId, | ||
| ), | ||
| async () => | ||
| await removeMonitoringDirectory( | ||
| application.appName, | ||
| application.serverId, | ||
| ), | ||
| async () => | ||
| await removeTraefikConfig( | ||
| application.appName, | ||
| application.serverId, | ||
| ), | ||
| async () => | ||
| await removeService(application?.appName, application.serverId), | ||
| ]; | ||
|
|
||
| for (const operation of cleanupOperations) { | ||
| try { | ||
| await operation(); | ||
| } catch (_) {} | ||
| } | ||
|
|
||
| return application; | ||
| return result[0]; |
There was a problem hiding this comment.
Return type changed from full entity to partial DB result
The original delete handler returned the full application object (fetched via findApplicationById). This change now returns result[0] from the raw db.delete().returning(), which is a different shape — it lacks the joined relations (e.g., environment.project) that the original application object had. If any frontend code depends on the returned object's structure from this mutation, it will break silently. The same issue exists in compose.delete at the corresponding location.
| const SENSITIVE_FIELDS = [ | ||
| "password", | ||
| "currentPassword", | ||
| "token", | ||
| "secret", | ||
| "key", | ||
| "env", | ||
| "buildArgs", | ||
| "secrets", | ||
| "apiKey", | ||
| ]; |
There was a problem hiding this comment.
Overly broad "key" field in SENSITIVE_FIELDS causes excessive redaction
The field name "key" matches any metadata key containing the substring "key" (case-insensitive), including non-sensitive ID fields like sshKeyId, customGitSSHKeyId, apiKeyId, backupId (no, but sshKey yes). For example, when logging SSH key creation, the resourceId field sshKeyId in the metadata object would show as [REDACTED] instead of the actual ID, making the audit log less useful.
Consider using more specific field names (e.g., "privateKey", "sshKey", "secretKey") or exact-match instead of substring matching to avoid redacting harmless ID fields.
| export const purgeActivityLogs = async ( | ||
| input: z.infer<typeof apiPurgeActivityLogsSchema>, | ||
| ) => { | ||
| const { organizationId, days } = input; | ||
|
|
||
| const date = new Date(); | ||
| date.setDate(date.getDate() - days); | ||
|
|
||
| const where = [lt(activityLogs.createdAt, date)]; | ||
|
|
||
| if (organizationId) { | ||
| where.push(eq(activityLogs.organizationId, organizationId)); | ||
| } | ||
|
|
||
| const result = await db | ||
| .delete(activityLogs) | ||
| .where(and(...where)) | ||
| .returning(); | ||
|
|
||
| return result.length; | ||
| }; |
There was a problem hiding this comment.
purgeActivityLogs with days=0 may not delete all logs
When days is 0, the computed date is set to the current timestamp. The WHERE clause uses lt(activityLogs.createdAt, date) (strictly less than), which means any logs created at or after the exact instant date was computed will not be deleted. The UI offers a "Clear All" option that passes days=0, but this won't reliably clear all logs — any log created in the same second (or between the timestamp computation and the query execution) will survive.
Consider using lte (less than or equal) when days === 0, or simply omitting the createdAt filter entirely for the "clear all" case.
| try { | ||
| if (IS_CLOUD && compose.serverId) { | ||
| jobData.serverId = compose.serverId; | ||
| deploy(jobData).catch((error) => { | ||
| console.error("Background deployment failed:", error); | ||
| }); | ||
| return true; |
There was a problem hiding this comment.
Cloud deployments skip audit logging
When IS_CLOUD && compose.serverId is true, the function returns true at line 470 before the recordActivity call at line 481 is reached. This means cloud deployments are never logged in the activity system. The same issue exists in the redeploy handler. The recordActivity call should be placed before the early return, or duplicated in both branches.
| await recordActivity({ | ||
| userId: ctx.user.id, | ||
| organizationId: ctx.session.activeOrganizationId, | ||
| action: "mariadb.delete", | ||
| resourceType: "database", | ||
| resourceId: mongo.mariadbId, | ||
| metadata: { | ||
| name: mongo.name, | ||
| appName: mongo.appName, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Wrong variable name: mongo used instead of mariadb
The recordActivity call references mongo.mariadbId, mongo.name, and mongo.appName, but this is the MariaDB delete handler. The variable is named mongo due to copy-paste from the Mongo router. While it happens to work because the local variable is indeed named mongo in the existing code (inherited from a code-generation pattern), this is misleading for audit log readers and introduces confusion. This same issue exists in mysql.ts where mongo.mysqlId is referenced.
…andling to application deploy/redeploy
…ssing auth check in mariadb stop
This PR introduces a robust and comprehensive audit logging system to Dokploy, providing full transparency and detailed tracking of user activities across the platform.
Latest Changes
redactSensitiveutility to recursively redact passwords, secrets, tokens, API keys, and other sensitive fields from activity log metadata.try/catchblocks in SSH key, Application, Compose, User, and Organization routers to ensure descriptiveTRPCErrormessages and prevent leaking raw database errors.pageSizeparameter ingetActivityLogsto prevent potential resource exhaustion.activity_logtable.canary.Key Improvements
Verification
Verified the system locally by generating logs for various actions. The new UI correctly displays resource names and provides deep insights via the metadata dialog.
This implementation significantly enhances the security and accountability of the Dokploy platform.
Greptile Summary
This PR introduces a comprehensive audit logging system that tracks user activities across Dokploy's routers — covering applications, compose, databases, servers, notifications, git providers, and more. It adds a new
activity_logtable, arecordActivityservice with sensitive data redaction, a TRPC router with proper authorization, and a React UI with filtering and pagination.5432to5433, which is unrelated to audit logging and will break existing dev environments.try/catchblocks (inapplication.update,compose.update,compose.delete,application.delete) wrapTRPCErrorinstances inside newTRPCErrors, causing the client to always receiveINTERNAL_SERVER_ERRORinstead of the original error code (e.g.,BAD_REQUEST).application.deleteandcompose.deletenow returnresult[0]from raw DB delete instead of the full entity object, changing the API contract for consumers.activity_logtable lacks indexes onorganizationIdandcreatedAt, which are used in every query. Performance will degrade as the table grows.compose.deployandcompose.redeploy, theIS_CLOUDcode path returns early beforerecordActivityis called."key"entry inSENSITIVE_FIELDSmatches field names likesshKeyIdandcustomGitSSHKeyId, unnecessarily redacting non-sensitive ID fields.days=0,purgeActivityLogsuses strict less-than on the current timestamp, which may not delete logs created in the same instant.Confidence Score: 2/5
packages/server/src/setup/postgres-setup.ts(unrelated breaking change),apps/dokploy/server/api/routers/application.ts(error handling and return type changes),apps/dokploy/server/api/routers/compose.ts(cloud deploy logging gap and return type),packages/server/src/services/activity-log.ts(redaction and purge logic), andapps/dokploy/drizzle/0144_plain_lady_ursula.sql(missing indexes).Last reviewed commit: 02554c0
(4/5) You can add custom instructions or style guidelines for the agent here!