diff --git a/apps/dokploy/__test__/schedule/schedule-router-boundary.test.ts b/apps/dokploy/__test__/schedule/schedule-router-boundary.test.ts index 9a3524dee1e..b6b35a35315 100644 --- a/apps/dokploy/__test__/schedule/schedule-router-boundary.test.ts +++ b/apps/dokploy/__test__/schedule/schedule-router-boundary.test.ts @@ -162,6 +162,50 @@ describe("schedule service/server binding boundary", () => { ); }); + it.each([ + { scheduleType: "dokploy-server" as const }, + { scheduleType: "server" as const, serverId: "server-1" }, + ])( + "rejects members creating $scheduleType schedules before persistence", + async (hostInput) => { + mocks.findMemberByUserId.mockResolvedValue({ role: "member" }); + + await expect( + createCaller().create({ + ...scheduleInput, + ...hostInput, + script: "id", + }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }); + + expect(mocks.createSchedule).not.toHaveBeenCalled(); + expect(mocks.scheduleJob).not.toHaveBeenCalled(); + }, + ); + + it("allows admins to create dokploy-server schedules", async () => { + mocks.createSchedule.mockResolvedValueOnce({ + ...scheduleInput, + scheduleId: "host-schedule-1", + scheduleType: "dokploy-server", + }); + + await expect( + createCaller().create({ + ...scheduleInput, + scheduleType: "dokploy-server", + script: "echo ok", + }), + ).resolves.toMatchObject({ scheduleId: "host-schedule-1" }); + + expect(mocks.createSchedule).toHaveBeenCalledWith( + expect.objectContaining({ + scheduleType: "dokploy-server", + organizationId: "org-1", + }), + ); + }); + it("rejects converting an application schedule into a server schedule", async () => { await expect( createCaller().update({ diff --git a/apps/dokploy/server/api/routers/schedule.ts b/apps/dokploy/server/api/routers/schedule.ts index 93f37f029d7..ede53d8e94b 100644 --- a/apps/dokploy/server/api/routers/schedule.ts +++ b/apps/dokploy/server/api/routers/schedule.ts @@ -162,6 +162,7 @@ export const scheduleRouter = createTRPCRouter({ } } else { await checkPermission(ctx, { schedule: ["create"] }); + await assertHostScheduleAccess(ctx, scheduleType, input.serverId); if (IS_CLOUD && input.scheduleType === "server" && input.serverId) { await assertScheduledJobLimit(