11import { IS_CLOUD , removeScheduleJob , scheduleJob } from "@dokploy/server" ;
22import { db } from "@dokploy/server/db" ;
3+ import { member } from "@dokploy/server/db/schema" ;
34import { deployments } from "@dokploy/server/db/schema/deployment" ;
45import {
56 createScheduleSchema ,
@@ -20,7 +21,7 @@ import {
2021} from "@dokploy/server/services/schedule" ;
2122import { findServerById } from "@dokploy/server/services/server" ;
2223import { TRPCError } from "@trpc/server" ;
23- import { asc , desc , eq } from "drizzle-orm" ;
24+ import { and , asc , desc , eq , inArray } from "drizzle-orm" ;
2425import { z } from "zod" ;
2526import { audit } from "@/server/api/utils/audit" ;
2627import { 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