mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
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:
committed by
Mauricio Siu
parent
30b3e1fe48
commit
c73632cbe0
11
apps/dokploy/drizzle/0169_parched_johnny_storm.sql
Normal file
11
apps/dokploy/drizzle/0169_parched_johnny_storm.sql
Normal file
@@ -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";
|
||||||
8332
apps/dokploy/drizzle/meta/0169_snapshot.json
Normal file
8332
apps/dokploy/drizzle/meta/0169_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1184,6 +1184,13 @@
|
|||||||
"when": 1780122833339,
|
"when": 1780122833339,
|
||||||
"tag": "0168_long_justice",
|
"tag": "0168_long_justice",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 169,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780127552074,
|
||||||
|
"tag": "0169_parched_johnny_storm",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -5,18 +5,13 @@ import type { ReactElement } from "react";
|
|||||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
function SchedulesPage() {
|
function SchedulesPage() {
|
||||||
const { data: user } = api.user.get.useQuery();
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
|
<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">
|
<div className="rounded-xl bg-background shadow-md h-full">
|
||||||
<ShowSchedules
|
<ShowSchedules scheduleType="dokploy-server" id="dokploy-server" />
|
||||||
scheduleType="dokploy-server"
|
|
||||||
id={user?.user.id || ""}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 (newSchedule?.enabled) {
|
||||||
if (IS_CLOUD) {
|
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);
|
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);
|
await deleteSchedule(input.scheduleId);
|
||||||
|
|
||||||
@@ -323,21 +306,27 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (input.scheduleType === "dokploy-server") {
|
||||||
input.scheduleType === "dokploy-server" &&
|
const member = await findMemberByUserId(
|
||||||
input.id !== ctx.user.id
|
ctx.user.id,
|
||||||
) {
|
ctx.session.activeOrganizationId,
|
||||||
throw new TRPCError({
|
);
|
||||||
code: "UNAUTHORIZED",
|
if (member.role !== "owner" && member.role !== "admin") {
|
||||||
message: "You can only list your own host-level schedules.",
|
throw new TRPCError({
|
||||||
});
|
code: "FORBIDDEN",
|
||||||
|
message: "Only owners and admins can list host-level schedules.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const where = {
|
const where = {
|
||||||
application: eq(schedules.applicationId, input.id),
|
application: eq(schedules.applicationId, input.id),
|
||||||
compose: eq(schedules.composeId, input.id),
|
compose: eq(schedules.composeId, input.id),
|
||||||
server: eq(schedules.serverId, 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({
|
return db.query.schedules.findMany({
|
||||||
where: where[input.scheduleType],
|
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;
|
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 {
|
try {
|
||||||
await runCommand(input.scheduleId);
|
await runCommand(input.scheduleId);
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
|||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { organization } from "./account";
|
||||||
import { applications } from "./application";
|
import { applications } from "./application";
|
||||||
import { compose } from "./compose";
|
import { compose } from "./compose";
|
||||||
import { deployments } from "./deployment";
|
import { deployments } from "./deployment";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import { user } from "./user";
|
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export const schedules = pgTable("schedule", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
userId: text("userId").references(() => user.id, {
|
organizationId: text("organizationId").references(() => organization.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
@@ -71,9 +71,9 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
|
|||||||
fields: [schedules.serverId],
|
fields: [schedules.serverId],
|
||||||
references: [server.serverId],
|
references: [server.serverId],
|
||||||
}),
|
}),
|
||||||
user: one(user, {
|
organization: one(organization, {
|
||||||
fields: [schedules.userId],
|
fields: [schedules.organizationId],
|
||||||
references: [user.id],
|
references: [organization.id],
|
||||||
}),
|
}),
|
||||||
deployments: many(deployments),
|
deployments: many(deployments),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export const findScheduleOrganizationId = async (scheduleId: string) => {
|
|||||||
if (schedule?.server) {
|
if (schedule?.server) {
|
||||||
return schedule?.server?.organization?.id;
|
return schedule?.server?.organization?.id;
|
||||||
}
|
}
|
||||||
|
if (schedule?.organizationId) {
|
||||||
|
return schedule.organizationId;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user