Files
dokploy/apps/dokploy/server/api/routers/volume-backups.ts
Mauricio Siu 8127dc4536 feat: add comprehensive permission tests and enhance permission checks in components
- Introduced new test files for permission checks, including `check-permission.test.ts`, `enterprise-only-resources.test.ts`, `resolve-permissions.test.ts`, and `service-access.test.ts`.
- Implemented permission checks in various components to ensure actions are gated by user permissions, including `ShowTraefikConfig`, `UpdateTraefikConfig`, `ShowVolumes`, `ShowDomains`, and others.
- Enhanced the logic for displaying UI elements based on user permissions, ensuring that only authorized users can access or modify resources.
2026-03-15 16:42:48 -06:00

316 lines
8.1 KiB
TypeScript

import {
createVolumeBackup,
findVolumeBackupById,
IS_CLOUD,
removeVolumeBackup,
removeVolumeBackupJob,
restoreVolume,
runVolumeBackup,
scheduleVolumeBackup,
updateVolumeBackup,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
createVolumeBackupSchema,
updateVolumeBackupSchema,
volumeBackups,
} from "@dokploy/server/db/schema";
import {
execAsyncRemote,
execAsyncStream,
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
id: z.string().min(1),
volumeBackupType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
]),
}),
)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.id, {
volumeBackup: ["read"],
});
return await db.query.volumeBackups.findMany({
where: eq(volumeBackups[`${input.volumeBackupType}Id`], input.id),
with: {
application: true,
postgres: true,
mysql: true,
mariadb: true,
mongo: true,
redis: true,
compose: true,
},
orderBy: [desc(volumeBackups.createdAt)],
});
}),
create: protectedProcedure
.input(createVolumeBackupSchema)
.mutation(async ({ input, ctx }) => {
const serviceId =
input.applicationId ||
input.postgresId ||
input.mysqlId ||
input.mariadbId ||
input.mongoId ||
input.redisId ||
input.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["create"],
});
}
const newVolumeBackup = await createVolumeBackup(input);
if (newVolumeBackup?.enabled) {
if (IS_CLOUD) {
await schedule({
cronSchedule: newVolumeBackup.cronExpression,
volumeBackupId: newVolumeBackup.volumeBackupId,
type: "volume-backup",
});
} else {
await scheduleVolumeBackup(newVolumeBackup.volumeBackupId);
}
}
await audit(ctx, {
action: "create",
resourceType: "volumeBackup",
resourceId: newVolumeBackup?.volumeBackupId,
});
return newVolumeBackup;
}),
one: protectedProcedure
.input(
z.object({
volumeBackupId: z.string().min(1),
}),
)
.query(async ({ input, ctx }) => {
const vb = await findVolumeBackupById(input.volumeBackupId);
const serviceId =
vb.applicationId ||
vb.postgresId ||
vb.mysqlId ||
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["read"],
});
}
return vb;
}),
delete: protectedProcedure
.input(
z.object({
volumeBackupId: z.string().min(1),
}),
)
.mutation(async ({ input, ctx }) => {
const vb = await findVolumeBackupById(input.volumeBackupId);
const serviceId =
vb.applicationId ||
vb.postgresId ||
vb.mysqlId ||
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["delete"],
});
}
const result = await removeVolumeBackup(input.volumeBackupId);
await audit(ctx, {
action: "delete",
resourceType: "volumeBackup",
resourceId: input.volumeBackupId,
});
return result;
}),
update: protectedProcedure
.input(updateVolumeBackupSchema)
.mutation(async ({ input, ctx }) => {
const existingVb = await findVolumeBackupById(input.volumeBackupId);
const serviceId =
existingVb.applicationId ||
existingVb.postgresId ||
existingVb.mysqlId ||
existingVb.mariadbId ||
existingVb.mongoId ||
existingVb.redisId ||
existingVb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["update"],
});
}
const updatedVolumeBackup = await updateVolumeBackup(
input.volumeBackupId,
input,
);
if (!updatedVolumeBackup) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Volume backup not found",
});
}
if (IS_CLOUD) {
if (updatedVolumeBackup.enabled) {
await updateJob({
cronSchedule: updatedVolumeBackup.cronExpression,
volumeBackupId: updatedVolumeBackup.volumeBackupId,
type: "volume-backup",
});
} else {
await removeJob({
cronSchedule: updatedVolumeBackup.cronExpression,
volumeBackupId: updatedVolumeBackup.volumeBackupId,
type: "volume-backup",
});
}
} else {
if (updatedVolumeBackup?.enabled) {
removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId);
scheduleVolumeBackup(updatedVolumeBackup.volumeBackupId);
} else {
removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId);
}
}
await audit(ctx, {
action: "update",
resourceType: "volumeBackup",
resourceId: updatedVolumeBackup.volumeBackupId,
});
return updatedVolumeBackup;
}),
runManually: protectedProcedure
.input(z.object({ volumeBackupId: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
const vb = await findVolumeBackupById(input.volumeBackupId);
const serviceId =
vb.applicationId ||
vb.postgresId ||
vb.mysqlId ||
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["create"],
});
}
try {
const result = await runVolumeBackup(input.volumeBackupId);
await audit(ctx, {
action: "run",
resourceType: "volumeBackup",
resourceId: input.volumeBackupId,
});
return result;
} catch (error) {
console.error(error);
return false;
}
}),
restoreVolumeBackupWithLogs: withPermission("volumeBackup", "restore")
.meta({
openapi: {
enabled: false,
path: "/restore-volume-backup-with-logs",
method: "POST",
override: true,
},
})
.input(
z.object({
backupFileName: z.string().min(1),
destinationId: z.string().min(1),
volumeName: z.string().min(1),
id: z.string().min(1),
serviceType: z.enum(["application", "compose"]),
serverId: z.string().optional(),
}),
)
.subscription(async ({ input }) => {
return observable<string>((emit) => {
const runRestore = async () => {
try {
emit.next("🚀 Starting volume restore process...");
emit.next(`📂 Backup File: ${input.backupFileName}`);
emit.next(`🔧 Volume Name: ${input.volumeName}`);
emit.next(`🏷️ Service Type: ${input.serviceType}`);
emit.next(""); // Empty line for better readability
// Generate the restore command
const restoreCommand = await restoreVolume(
input.id,
input.destinationId,
input.volumeName,
input.backupFileName,
input.serverId || "",
input.serviceType,
);
emit.next("📋 Generated restore command:");
emit.next("▶️ Executing restore...");
emit.next(""); // Empty line
// Execute the restore command with real-time output
if (input.serverId) {
emit.next(`🌐 Executing on remote server: ${input.serverId}`);
await execAsyncRemote(input.serverId, restoreCommand, (data) => {
emit.next(data);
});
} else {
emit.next("🖥️ Executing on local server");
await execAsyncStream(restoreCommand, (data) => {
emit.next(data);
});
}
emit.next("");
emit.next("✅ Volume restore completed successfully!");
emit.next(
"🎉 All containers/services have been restarted with the restored volume.",
);
} catch {
emit.next("");
emit.next("❌ Volume restore failed!");
} finally {
emit.complete();
}
};
// Start the restore process
runRestore();
});
}),
});