mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-07 15:05:23 +02:00
Prevents owner/admin users of one organization from accessing servers, destinations, and Docker Swarm join tokens belonging to other organizations by validating organizationId on all endpoints that accept serverId or destinationId as direct input. - cluster: validate serverId org on getNodes, addWorker, addManager, removeWorker - deployment: validate serverId org on allByServer - backup: validate destinationId + serverId org on listBackupFiles - volume-backups: validate destinationId + serverId org on restoreVolumeBackupWithLogs - wss: validate server org on docker-container-logs, docker-container-terminal, listen-deployment, and terminal WebSocket handlers - auth: fix TypeScript type for API key metadata parsing
341 lines
9.0 KiB
TypeScript
341 lines
9.0 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 { findDestinationById } from "@dokploy/server/services/destination";
|
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
|
import { findServerById } from "@dokploy/server/services/server";
|
|
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 { 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",
|
|
"libsql",
|
|
]),
|
|
}),
|
|
)
|
|
.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,
|
|
libsql: 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.libsqlId ||
|
|
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.libsqlId ||
|
|
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.libsqlId ||
|
|
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.libsqlId ||
|
|
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.libsqlId ||
|
|
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, ctx }) => {
|
|
const destination = await findDestinationById(input.destinationId);
|
|
if (destination.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You don't have access to this destination.",
|
|
});
|
|
}
|
|
if (input.serverId) {
|
|
const targetServer = await findServerById(input.serverId);
|
|
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You don't have access to this server.",
|
|
});
|
|
}
|
|
}
|
|
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();
|
|
});
|
|
}),
|
|
});
|