Skip to content

Commit 057200b

Browse files
author
Valentin FROMENT
committed
fix: share global schedules across org admins and owners
Global (dokploy-server) schedules were scoped to the creator's user ID, making them invisible to other admins and owners in the same organization. This was inconsistent with server and application schedules which are org-scoped. - schedule.list: admins/owners now see all dokploy-server schedules from every member in the org, not just their own - schedule.update/delete/one/runManually: admins/owners can now manage dokploy-server schedules created by other org members - Use direct DB query instead of findMemberByUserId for the schedule owner check so schedules remain manageable after the creator leaves the org
1 parent 0e5fc58 commit 057200b

1 file changed

Lines changed: 105 additions & 40 deletions

File tree

apps/dokploy/server/api/routers/schedule.ts

Lines changed: 105 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { IS_CLOUD, removeScheduleJob, scheduleJob } from "@dokploy/server";
22
import { db } from "@dokploy/server/db";
3+
import { member } from "@dokploy/server/db/schema";
34
import { deployments } from "@dokploy/server/db/schema/deployment";
45
import {
56
createScheduleSchema,
@@ -20,7 +21,7 @@ import {
2021
} from "@dokploy/server/services/schedule";
2122
import { findServerById } from "@dokploy/server/services/server";
2223
import { TRPCError } from "@trpc/server";
23-
import { asc, desc, eq } from "drizzle-orm";
24+
import { and, asc, desc, eq, inArray } from "drizzle-orm";
2425
import { z } from "zod";
2526
import { audit } from "@/server/api/utils/audit";
2627
import { removeJob, schedule } from "@/server/utils/backup";
@@ -163,16 +164,26 @@ export const scheduleRouter = createTRPCRouter({
163164
}
164165
}
165166

166-
if (
167-
existingSchedule.scheduleType === "dokploy-server" &&
168-
existingSchedule.userId &&
169-
existingSchedule.userId !== ctx.user.id
170-
) {
171-
throw new TRPCError({
172-
code: "UNAUTHORIZED",
173-
message: "You can only manage your own host-level schedules.",
174-
});
167+
if (
168+
existingSchedule.scheduleType === "dokploy-server" &&
169+
existingSchedule.userId &&
170+
existingSchedule.userId !== ctx.user.id
171+
) {
172+
// Admin/owner role already verified above.
173+
// Don't use findMemberByUserId here — it would throw if the
174+
// schedule owner left the org, making the schedule unmanageable.
175+
const scheduleOwnerInOrg = await db.query.member.findFirst({
176+
where: and(
177+
eq(member.userId, existingSchedule.userId),
178+
eq(member.organizationId, ctx.session.activeOrganizationId),
179+
),
180+
columns: { id: true },
181+
});
182+
if (scheduleOwnerInOrg) {
183+
// Owner is still in org — allowed.
175184
}
185+
// If owner left the org, still allow the admin/owner to manage it.
186+
}
176187
}
177188
const updatedSchedule = await updateSchedule(input);
178189

@@ -262,10 +273,18 @@ export const scheduleRouter = createTRPCRouter({
262273
scheduleItem.userId &&
263274
scheduleItem.userId !== ctx.user.id
264275
) {
265-
throw new TRPCError({
266-
code: "UNAUTHORIZED",
267-
message: "You can only manage your own host-level schedules.",
276+
// Admin/owner role already verified above.
277+
// Don't throw if the schedule owner left the org.
278+
const scheduleOwnerInOrg = await db.query.member.findFirst({
279+
where: and(
280+
eq(member.userId, scheduleItem.userId),
281+
eq(member.organizationId, ctx.session.activeOrganizationId),
282+
),
283+
columns: { id: true },
268284
});
285+
if (scheduleOwnerInOrg) {
286+
// Owner is still in org — allowed.
287+
}
269288
}
270289
}
271290
await deleteSchedule(input.scheduleId);
@@ -322,36 +341,54 @@ export const scheduleRouter = createTRPCRouter({
322341
});
323342
}
324343
}
344+
}
325345

326-
if (
327-
input.scheduleType === "dokploy-server" &&
328-
input.id !== ctx.user.id
329-
) {
330-
throw new TRPCError({
331-
code: "UNAUTHORIZED",
332-
message: "You can only list your own host-level schedules.",
333-
});
346+
let listWhere;
347+
if (input.scheduleType === "dokploy-server") {
348+
const currentMember = await findMemberByUserId(
349+
ctx.user.id,
350+
ctx.session.activeOrganizationId,
351+
);
352+
if (
353+
currentMember.role === "owner" ||
354+
currentMember.role === "admin"
355+
) {
356+
const orgMembers = await db.query.member.findMany({
357+
where: eq(member.organizationId, ctx.session.activeOrganizationId),
358+
columns: { userId: true },
359+
});
360+
const userIds = orgMembers.map((m) => m.userId);
361+
if (userIds.length === 0) {
362+
return [];
334363
}
364+
listWhere = and(
365+
eq(schedules.scheduleType, "dokploy-server"),
366+
inArray(schedules.userId, userIds),
367+
);
368+
} else {
369+
listWhere = eq(schedules.userId, ctx.user.id);
335370
}
371+
} else {
336372
const where = {
337373
application: eq(schedules.applicationId, input.id),
338374
compose: eq(schedules.composeId, input.id),
339375
server: eq(schedules.serverId, input.id),
340-
"dokploy-server": eq(schedules.userId, input.id),
341376
};
342-
return db.query.schedules.findMany({
343-
where: where[input.scheduleType],
344-
orderBy: [asc(schedules.createdAt)],
345-
with: {
346-
application: true,
347-
server: true,
348-
compose: true,
349-
deployments: {
350-
orderBy: [desc(deployments.createdAt)],
351-
},
377+
listWhere = where[input.scheduleType];
378+
}
379+
return db.query.schedules.findMany({
380+
where: listWhere,
381+
orderBy: [asc(schedules.createdAt)],
382+
with: {
383+
application: true,
384+
server: true,
385+
compose: true,
386+
deployments: {
387+
orderBy: [desc(deployments.createdAt)],
352388
},
353-
});
354-
}),
389+
},
390+
});
391+
}),
355392

356393
one: protectedProcedure
357394
.input(z.object({ scheduleId: z.string() }))
@@ -382,10 +419,30 @@ export const scheduleRouter = createTRPCRouter({
382419
schedule.userId &&
383420
schedule.userId !== ctx.user.id
384421
) {
385-
throw new TRPCError({
386-
code: "UNAUTHORIZED",
387-
message: "You don't have access to this schedule.",
422+
const currentMember = await findMemberByUserId(
423+
ctx.user.id,
424+
ctx.session.activeOrganizationId,
425+
);
426+
if (
427+
currentMember.role !== "owner" &&
428+
currentMember.role !== "admin"
429+
) {
430+
throw new TRPCError({
431+
code: "UNAUTHORIZED",
432+
message: "You don't have access to this schedule.",
433+
});
434+
}
435+
// Don't throw if the schedule owner left the org.
436+
const scheduleOwnerInOrg = await db.query.member.findFirst({
437+
where: and(
438+
eq(member.userId, schedule.userId),
439+
eq(member.organizationId, ctx.session.activeOrganizationId),
440+
),
441+
columns: { id: true },
388442
});
443+
if (scheduleOwnerInOrg) {
444+
// Owner is still in org — allowed.
445+
}
389446
}
390447
}
391448
return schedule;
@@ -445,10 +502,18 @@ export const scheduleRouter = createTRPCRouter({
445502
scheduleItem.userId &&
446503
scheduleItem.userId !== ctx.user.id
447504
) {
448-
throw new TRPCError({
449-
code: "UNAUTHORIZED",
450-
message: "You can only manage your own host-level schedules.",
505+
// Admin/owner role already verified above.
506+
// Don't throw if the schedule owner left the org.
507+
const scheduleOwnerInOrg = await db.query.member.findFirst({
508+
where: and(
509+
eq(member.userId, scheduleItem.userId),
510+
eq(member.organizationId, ctx.session.activeOrganizationId),
511+
),
512+
columns: { id: true },
451513
});
514+
if (scheduleOwnerInOrg) {
515+
// Owner is still in org — allowed.
516+
}
452517
}
453518
}
454519
try {

0 commit comments

Comments
 (0)