fix: scope dokploy-server schedules to organization instead of user (#4526)

* fix: scope dokploy-server schedules to organization instead of user

Replaces userId with organizationId on the schedule table so that
global (dokploy-server) schedules are shared across all owners and
admins of the same organization, while remaining isolated between
different organizations.

Includes a data migration that backfills organizationId from the
owner membership record for any existing dokploy-server schedules.

Closes #4300

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Mauricio Siu
2026-05-31 15:29:05 -06:00
committed by Mauricio Siu
parent 30b3e1fe48
commit c73632cbe0
7 changed files with 8381 additions and 66 deletions
@@ -0,0 +1,11 @@
ALTER TABLE "schedule" DROP CONSTRAINT "schedule_userId_user_id_fk";
--> statement-breakpoint
ALTER TABLE "schedule" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
UPDATE "schedule" s
SET "organizationId" = m."organization_id"
FROM "member" m
WHERE s."scheduleType" = 'dokploy-server'
AND s."userId" = m."user_id"
AND m."role" = 'owner';--> statement-breakpoint
ALTER TABLE "schedule" DROP COLUMN "userId";
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -1184,6 +1184,13 @@
"when": 1780122833339,
"tag": "0168_long_justice",
"breakpoints": true
},
{
"idx": 169,
"version": "7",
"when": 1780127552074,
"tag": "0169_parched_johnny_storm",
"breakpoints": true
}
]
}
}
+1 -6
View File
@@ -5,18 +5,13 @@ import type { ReactElement } from "react";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
function SchedulesPage() {
const { data: user } = api.user.get.useQuery();
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
<div className="rounded-xl bg-background shadow-md h-full">
<ShowSchedules
scheduleType="dokploy-server"
id={user?.user.id || ""}
/>
<ShowSchedules scheduleType="dokploy-server" id="dokploy-server" />
</div>
</Card>
</div>
+21 -54
View File
@@ -75,7 +75,12 @@ export const scheduleRouter = createTRPCRouter({
}
}
}
const newSchedule = await createSchedule(input);
const newSchedule = await createSchedule({
...input,
...(input.scheduleType === "dokploy-server" && {
organizationId: ctx.session.activeOrganizationId,
}),
});
if (newSchedule?.enabled) {
if (IS_CLOUD) {
@@ -162,17 +167,6 @@ export const scheduleRouter = createTRPCRouter({
});
}
}
if (
existingSchedule.scheduleType === "dokploy-server" &&
existingSchedule.userId &&
existingSchedule.userId !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You can only manage your own host-level schedules.",
});
}
}
const updatedSchedule = await updateSchedule(input);
@@ -256,17 +250,6 @@ export const scheduleRouter = createTRPCRouter({
});
}
}
if (
scheduleItem.scheduleType === "dokploy-server" &&
scheduleItem.userId &&
scheduleItem.userId !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You can only manage your own host-level schedules.",
});
}
}
await deleteSchedule(input.scheduleId);
@@ -323,21 +306,27 @@ export const scheduleRouter = createTRPCRouter({
}
}
if (
input.scheduleType === "dokploy-server" &&
input.id !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You can only list your own host-level schedules.",
});
if (input.scheduleType === "dokploy-server") {
const member = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (member.role !== "owner" && member.role !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Only owners and admins can list host-level schedules.",
});
}
}
}
const where = {
application: eq(schedules.applicationId, input.id),
compose: eq(schedules.composeId, input.id),
server: eq(schedules.serverId, input.id),
"dokploy-server": eq(schedules.userId, input.id),
"dokploy-server": eq(
schedules.organizationId,
ctx.session.activeOrganizationId,
),
};
return db.query.schedules.findMany({
where: where[input.scheduleType],
@@ -376,17 +365,6 @@ export const scheduleRouter = createTRPCRouter({
});
}
}
if (
schedule.scheduleType === "dokploy-server" &&
schedule.userId &&
schedule.userId !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this schedule.",
});
}
}
return schedule;
}),
@@ -439,17 +417,6 @@ export const scheduleRouter = createTRPCRouter({
});
}
}
if (
scheduleItem.scheduleType === "dokploy-server" &&
scheduleItem.userId &&
scheduleItem.userId !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You can only manage your own host-level schedules.",
});
}
}
try {
await runCommand(input.scheduleId);
+5 -5
View File
@@ -3,11 +3,11 @@ import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
import { applications } from "./application";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { server } from "./server";
import { user } from "./user";
import { generateAppName } from "./utils";
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
@@ -46,7 +46,7 @@ export const schedules = pgTable("schedule", {
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
userId: text("userId").references(() => user.id, {
organizationId: text("organizationId").references(() => organization.id, {
onDelete: "cascade",
}),
enabled: boolean("enabled").notNull().default(true),
@@ -71,9 +71,9 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
fields: [schedules.serverId],
references: [server.serverId],
}),
user: one(user, {
fields: [schedules.userId],
references: [user.id],
organization: one(organization, {
fields: [schedules.organizationId],
references: [organization.id],
}),
deployments: many(deployments),
}));
+3
View File
@@ -85,6 +85,9 @@ export const findScheduleOrganizationId = async (scheduleId: string) => {
if (schedule?.server) {
return schedule?.server?.organization?.id;
}
if (schedule?.organizationId) {
return schedule.organizationId;
}
return null;
};