mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 21:55:24 +02:00
- 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.
316 lines
8.1 KiB
TypeScript
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();
|
|
});
|
|
}),
|
|
});
|