{"record":{"id":"be4a9f83eef164c0","repo":"calcom/cal.diy","slug":"user-with-id-userid-does-not-own-schedule-with-be4a9f","errorCode":null,"errorMessage":"User with ID=${userId} does not own schedule with ID=${schedule.id}","messagePattern":"User with ID=(.+?) does not own schedule with ID=(.+?)","errorType":"http","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/schedules.service.ts","lineNumber":179,"sourceCode":"\n    return this.outputSchedulesService.getResponseSchedule(updatedSchedule);\n  }\n\n  async deleteUserSchedule(userId: number, scheduleId: number): Promise<Schedule> {\n    const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);\n\n    if (!existingSchedule) {\n      throw new BadRequestException(`Schedule with ID=${scheduleId} does not exist.`);\n    }\n\n    this.checkUserOwnsSchedule(userId, existingSchedule);\n\n    return this.schedulesRepository.deleteScheduleById(scheduleId);\n  }\n\n  checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, \"id\" | \"userId\">): void {\n    if (userId !== schedule.userId) {\n      throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);\n    }\n  }\n}\n","sourceCodeStart":161,"sourceCodeEnd":183,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/schedules.service.ts#L161-L183","documentation":"Thrown by checkUserOwnsSchedule (a NestJS ForbiddenException → HTTP 403) when the resolved userId does not equal schedule.userId. It runs only after the schedule is confirmed to exist, so a 403 here means the resource belongs to a different user.","triggerScenarios":"Authenticated user A calls DELETE on a schedule owned by user B; a session/token was reused across accounts; an admin-style client passes an arbitrary scheduleId without scoping it to the caller.","commonSituations":"Cross-tenant access attempts, shared/leaked URL containing another user's scheduleId, front-end bug reusing a cached selected schedule after account switch.","solutions":["Scope the delete request to the current user's own schedules only (fetch the list first, as in 460).","On the client, clear cached schedule state on logout / account switch to prevent stale IDs leaking across users.","If legitimate admin deletion is needed, route through an authorized admin endpoint rather than the user-scoped one.","Surface a 'you do not have permission' UI on HTTP 403 instead of retrying."],"exampleFix":"// before\nawait schedulesService.deleteUserSchedule(currentUserId, arbitraryScheduleId);\n\n// after\nconst owned = await schedulesRepository.getScheduleById(scheduleId);\nif (owned?.userId !== currentUserId) {\n  throw new ForbiddenError('not your schedule');\n}\nawait schedulesService.deleteUserSchedule(currentUserId, scheduleId);","handlingStrategy":"validation","validationCode":"// Verify ownership client-side using the same user context the API checks\nconst schedule = schedules.find(s => s.id === scheduleId);\nif (!schedule || schedule.userId !== session.user.id) {\n  showError('You can only delete your own schedules.');\n  return;\n}\nawait api.delete(`/v2/schedules/${scheduleId}`);","typeGuard":"function isOwnedBy(schedule: unknown, userId: number): schedule is { id: number; userId: number } {\n  return !!schedule && typeof schedule === 'object' &&\n    (schedule as any).userId === userId;\n}","tryCatchPattern":"try {\n  await schedulesApi.delete(scheduleId);\n} catch (e) {\n  if (e instanceof HttpError && e.statusCode === 403) {\n    notify('You do not have permission to delete this schedule.');\n    return;\n  }\n  throw e;\n}","preventionTips":["Only show delete controls for schedules where schedule.userId === currentUserId.","Clear cached selected schedule on account switch / logout.","Never pass arbitrary user-supplied scheduleIds to the user-scoped delete endpoint."],"tags":["schedules","authorization","api-v2","nestjs","forbidden","ownership"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}