fix(security): enforce org-scope on move and update procedures

This commit is contained in:
detail-app[bot] 2026-09-04 02:53:22 +00:00 committed by GitHub
parent 1572008cdf
commit 20dd8a2c37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 741 additions and 8 deletions

View File

@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
// The cross-org re-parenting vector through `application.update` exists for every
// service type: each apiUpdate* schema derives from createSchema.partial() and
// previously only omitted serverId, leaving environmentId settable through the
// `update` mutation with no target-env authorization. The fix omits environmentId
// from every apiUpdate* schema. These tests pin that contract so a future
// createSchema/.partial() refactor cannot silently re-introduce it.
import {
apiUpdateCompose,
apiUpdateLibsql,
apiUpdateMariaDB,
apiUpdateMongo,
apiUpdateMySql,
apiUpdatePostgres,
apiUpdateRedis,
} from "@/server/db/schema";
const schemas = {
apiUpdatePostgres: {
schema: apiUpdatePostgres,
idField: "postgresId",
extra: { dockerImage: "postgres:16" },
},
apiUpdateRedis: {
schema: apiUpdateRedis,
idField: "redisId",
extra: { dockerImage: "redis:7" },
},
apiUpdateMariaDB: {
schema: apiUpdateMariaDB,
idField: "mariadbId",
extra: { dockerImage: "mariadb:11" },
},
apiUpdateMySql: {
schema: apiUpdateMySql,
idField: "mysqlId",
extra: { dockerImage: "mysql:8" },
},
apiUpdateMongo: {
schema: apiUpdateMongo,
idField: "mongoId",
extra: { dockerImage: "mongo:7" },
},
apiUpdateLibsql: { schema: apiUpdateLibsql, idField: "libsqlId", extra: {} },
apiUpdateCompose: {
schema: apiUpdateCompose,
idField: "composeId",
extra: { composeFile: "services:\n web:\n image: nginx" },
},
};
describe("apiUpdate* schemas omit environmentId (no re-parenting via update)", () => {
for (const [name, { schema, idField, extra }] of Object.entries(schemas)) {
describe(name, () => {
it("drops environmentId from the parsed payload", () => {
const parsed = schema.parse({
[idField]: "svc-1",
environmentId: "foreign-env",
} as Record<string, unknown>);
expect(parsed).not.toHaveProperty("environmentId");
});
it("still parses legitimate update fields", () => {
const parsed = schema.parse({
[idField]: "svc-1",
...extra,
} as Record<string, unknown>);
expect(parsed).toHaveProperty(idField, "svc-1");
for (const [k, v] of Object.entries(extra)) {
expect(parsed).toHaveProperty(k, v);
}
});
});
}
});

View File

@ -0,0 +1,207 @@
import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
// Bridge to the application router is exercised through tRPC's createCaller so
// the org-scope guards that live inline in the procedure (restored by the fix)
// are verified exactly as a real client request would hit them.
//
// The DB is mocked one layer down (same pattern as git-provider-idor.test.ts)
// so the real findApplicationById / findEnvironmentById / checkServicePermissionAndAccess
// run against controlled org/project tree data.
const mockDb = vi.hoisted(() => {
const returning = vi.fn(() =>
Promise.resolve([{ applicationId: "app-1", appName: "app-1" }]),
);
const set = vi.fn(() => ({ where: () => ({ returning }) }));
const update = vi.fn(() => ({ set }));
return {
update,
_ret: returning,
_set: set,
query: {
member: { findFirst: vi.fn() },
organizationRole: { findMany: vi.fn(() => Promise.resolve([])) },
applications: { findFirst: vi.fn() },
environments: { findFirst: vi.fn() },
},
};
});
vi.mock("@dokploy/server/db", () => ({ db: mockDb }));
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
hasValidLicense: vi.fn(() => Promise.resolve(false)),
}));
vi.mock("@dokploy/server/services/proprietary/audit-log", () => ({
createAuditLog: vi.fn(() => Promise.resolve()),
}));
// getAccessibleServerIds is only used by the application.update procedure when
// buildServerId is supplied; keep it deterministic in case it is reached.
vi.mock("@dokploy/server/services/server", () => ({
getAccessibleServerIds: vi.fn(() => Promise.resolve(new Set())),
}));
import { applicationRouter } from "@/server/api/routers/application";
const ORG_A = "org-a";
const ORG_B = "org-b";
// tRPC's createCaller is typed against the full CreateContextOptions (db/req/res
// + the full better-auth Session). The procedures only read `ctx.user` and
// `ctx.session.activeOrganizationId`; typing the helper as `any` lets createCaller
// accept the minimal ctx while the returned caller stays fully typed by the router.
const ctx = (role: "owner" | "admin" | "member", org = ORG_A): any => ({
user: {
id: "user-1",
email: "owner@example.com",
role,
ownerId: "owner-1",
},
session: { activeOrganizationId: org },
});
const appRow = (orgId: string) => ({
applicationId: "app-1",
appName: "app-1",
environment: { project: { organizationId: orgId } },
});
const envRow = (orgId: string) => ({
environmentId: "env-target",
project: { organizationId: orgId },
});
const memberRow = (
role: "owner" | "admin" | "member",
accessedServices: string[] = ["app-1"],
) => ({
id: "member-1",
role,
userId: "user-1",
organizationId: ORG_A,
accessedServices,
accessedProjects: [],
accessedEnvironments: [],
canCreateServices: true,
canCreateProjects: true,
canCreateEnvironments: true,
canDeleteServices: true,
canDeleteProjects: true,
canDeleteEnvironments: true,
canAccessToTraefikFiles: true,
canAccessToDocker: true,
canAccessToAPI: true,
canAccessToSSHKeys: true,
canAccessToGitProviders: true,
user: { id: "user-1", email: "owner@example.com" },
});
beforeEach(() => {
vi.clearAllMocks();
mockDb.query.member.findFirst.mockResolvedValue(memberRow("owner"));
mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_A));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A));
mockDb.query.organizationRole.findMany.mockResolvedValue([]);
mockDb._ret.mockResolvedValue([{ applicationId: "app-1", appName: "app-1" }]);
});
describe("application.move org-scope guards", () => {
it("allows moving an application between environments of the same organization", async () => {
mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_A));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A));
const caller = applicationRouter.createCaller(ctx("owner"));
await expect(
caller.move({
applicationId: "app-1",
targetEnvironmentId: "env-target",
}),
).resolves.toMatchObject({ applicationId: "app-1" });
expect(mockDb.update).toHaveBeenCalledOnce();
});
it("rejects moving an application that belongs to a foreign organization (source check)", async () => {
// Headline exfiltration vector: owner of Org A pulls an Org B app into an Org A env.
mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_B));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A));
const caller = applicationRouter.createCaller(ctx("owner"));
await expect(
caller.move({
applicationId: "app-1",
targetEnvironmentId: "env-target",
}),
).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "You are not authorized to move this application",
});
// The DB write must never run for a rejected move.
expect(mockDb.update).not.toHaveBeenCalled();
});
it("rejects moving to a target environment that belongs to a foreign organization (target check)", async () => {
mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_A));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_B));
const caller = applicationRouter.createCaller(ctx("owner"));
await expect(
caller.move({
applicationId: "app-1",
targetEnvironmentId: "env-target",
}),
).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
expect(mockDb.update).not.toHaveBeenCalled();
});
it("still rejects the foreign-source move for an owner even when accessedServices would otherwise be skipped", async () => {
// Owner role bypasses the accessedServices list, so the only thing standing
// between a foreign applicationId and re-parenting is the restored source check.
mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_B));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A));
const caller = applicationRouter.createCaller(ctx("owner"));
await expect(
caller.move({
applicationId: "app-foreign",
targetEnvironmentId: "env-target",
}),
).rejects.toMatchObject({ code: "UNAUTHORIZED" });
expect(mockDb.update).not.toHaveBeenCalled();
});
it("rejects a non-owner (with access to the source app) moving to a foreign organization (orphaning path)", async () => {
mockDb.query.member.findFirst.mockResolvedValue(
memberRow("member", ["app-1"]),
);
mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_A));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_B));
const caller = applicationRouter.createCaller(ctx("member"));
await expect(
caller.move({
applicationId: "app-1",
targetEnvironmentId: "env-target",
}),
).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
expect(mockDb.update).not.toHaveBeenCalled();
});
it("throws a TRPCError (so tRPC maps the HTTP status) on the source check", async () => {
mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_B));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A));
const caller = applicationRouter.createCaller(ctx("owner"));
const err = await caller
.move({ applicationId: "app-1", targetEnvironmentId: "env-target" })
.catch((e) => e);
expect(err).toBeInstanceOf(TRPCError);
});
});

View File

@ -0,0 +1,125 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
// The `application.update` mutation previously accepted `environmentId` via the
// `apiUpdateApplication` schema (derived from createSchema.partial()) and passed
// it straight through to updateApplication -> db.update(applications).set(...).
// That was a second cross-org re-parenting path with no target-env authorization.
// The fix omits `environmentId` from apiUpdateApplication so the field can no longer
// be set through update — environment changes must go through the guarded `move`.
const mockDb = vi.hoisted(() => {
const returning = vi.fn(() =>
Promise.resolve([{ applicationId: "app-1", appName: "app-1" }]),
);
const set = vi.fn((_data: Record<string, unknown>) => ({
where: () => ({ returning }),
}));
const update = vi.fn(() => ({ set }));
return {
update,
_set: set,
_ret: returning,
query: {
member: { findFirst: vi.fn() },
organizationRole: { findMany: vi.fn(() => Promise.resolve([])) },
},
};
});
vi.mock("@dokploy/server/db", () => ({ db: mockDb }));
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
hasValidLicense: vi.fn(() => Promise.resolve(false)),
}));
vi.mock("@dokploy/server/services/proprietary/audit-log", () => ({
createAuditLog: vi.fn(() => Promise.resolve()),
}));
vi.mock("@dokploy/server/services/server", () => ({
getAccessibleServerIds: vi.fn(() => Promise.resolve(new Set())),
}));
import { applicationRouter } from "@/server/api/routers/application";
import { apiUpdateApplication } from "@/server/db/schema";
const ORG_A = "org-a";
const ctx = (): any => ({
user: {
id: "user-1",
email: "owner@example.com",
role: "owner" as const,
ownerId: "owner-1",
},
session: { activeOrganizationId: ORG_A },
});
const memberRow = () => ({
id: "member-1",
role: "owner",
userId: "user-1",
organizationId: ORG_A,
accessedServices: ["app-1"],
accessedProjects: [],
accessedEnvironments: [],
canCreateServices: true,
canCreateProjects: true,
canCreateEnvironments: true,
canDeleteServices: true,
canDeleteProjects: true,
canDeleteEnvironments: true,
canAccessToTraefikFiles: true,
canAccessToDocker: true,
canAccessToAPI: true,
canAccessToSSHKeys: true,
canAccessToGitProviders: true,
user: { id: "user-1", email: "owner@example.com" },
});
beforeEach(() => {
vi.clearAllMocks();
mockDb.query.member.findFirst.mockResolvedValue(memberRow());
mockDb.query.organizationRole.findMany.mockResolvedValue([]);
mockDb._ret.mockResolvedValue([{ applicationId: "app-1", appName: "app-1" }]);
});
describe("apiUpdateApplication schema (environmentId omitted)", () => {
it("does not expose environmentId as a parsed key", () => {
const parsed = apiUpdateApplication.parse({
applicationId: "app-1",
environmentId: "foreign-env",
autoDeploy: true,
});
expect(parsed).not.toHaveProperty("environmentId");
expect(parsed).toHaveProperty("applicationId", "app-1");
expect(parsed).toHaveProperty("autoDeploy", true);
});
it("silently drops environmentId supplied as an unknown key", () => {
const parsed = apiUpdateApplication.parse({
applicationId: "app-1",
environmentId: "foreign-env",
});
expect(Object.keys(parsed)).not.toContain("environmentId");
});
});
describe("application.update does not re-parent via environmentId", () => {
it("ignores environmentId in the mutation payload and never writes it to the db", async () => {
const caller = applicationRouter.createCaller(ctx());
// A raw client could still send `environmentId` over the wire; zod now strips
// it (it's omitted from apiUpdateApplication). Cast to simulate that payload.
await caller.update({
applicationId: "app-1",
environmentId: "foreign-env",
autoDeploy: true,
} as any);
expect(mockDb._set).toHaveBeenCalledOnce();
const setData = mockDb._set.mock.calls[0]![0];
expect(setData).not.toHaveProperty("environmentId");
// Sanity: the legitimate field still flows through.
expect(setData).toHaveProperty("autoDeploy", true);
});
});

View File

@ -0,0 +1,132 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
// Representative sibling guard: the same org-scope checks restored on
// application.move were also restored on the database-service `move` procedures
// (postgres, redis, mariadb, mysql, mongo, libsql, compose). Postgres is exercised
// here through tRPC's createCaller; the remaining siblings share the identical
// procedure shape and are covered by the schema + manual verification plan.
const mockDb = vi.hoisted(() => {
const returning = vi.fn(() =>
Promise.resolve([{ postgresId: "pg-1", appName: "pg-1" }]),
);
const set = vi.fn(() => ({ where: () => ({ returning }) }));
const update = vi.fn(() => ({ set }));
return {
update,
_set: set,
_ret: returning,
query: {
member: { findFirst: vi.fn() },
organizationRole: { findMany: vi.fn(() => Promise.resolve([])) },
postgres: { findFirst: vi.fn() },
environments: { findFirst: vi.fn() },
},
};
});
vi.mock("@dokploy/server/db", () => ({ db: mockDb }));
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
hasValidLicense: vi.fn(() => Promise.resolve(false)),
}));
vi.mock("@dokploy/server/services/proprietary/audit-log", () => ({
createAuditLog: vi.fn(() => Promise.resolve()),
}));
import { postgresRouter } from "@/server/api/routers/postgres";
const ORG_A = "org-a";
const ORG_B = "org-b";
const ctx = (): any => ({
user: {
id: "user-1",
email: "owner@example.com",
role: "owner" as const,
ownerId: "owner-1",
},
session: { activeOrganizationId: ORG_A },
});
const pgRow = (orgId: string) => ({
postgresId: "pg-1",
appName: "pg-1",
environment: { project: { organizationId: orgId } },
});
const envRow = (orgId: string) => ({
environmentId: "env-target",
project: { organizationId: orgId },
});
const memberRow = () => ({
id: "member-1",
role: "owner",
userId: "user-1",
organizationId: ORG_A,
accessedServices: ["pg-1"],
accessedProjects: [],
accessedEnvironments: [],
canCreateServices: true,
canCreateProjects: true,
canCreateEnvironments: true,
canDeleteServices: true,
canDeleteProjects: true,
canDeleteEnvironments: true,
canAccessToTraefikFiles: true,
canAccessToDocker: true,
canAccessToAPI: true,
canAccessToSSHKeys: true,
canAccessToGitProviders: true,
user: { id: "user-1", email: "owner@example.com" },
});
beforeEach(() => {
vi.clearAllMocks();
mockDb.query.member.findFirst.mockResolvedValue(memberRow());
mockDb.query.postgres.findFirst.mockResolvedValue(pgRow(ORG_A));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A));
mockDb.query.organizationRole.findMany.mockResolvedValue([]);
});
describe("postgres.move org-scope guards", () => {
it("allows moving a postgres within the same organization", async () => {
mockDb.query.postgres.findFirst.mockResolvedValue(pgRow(ORG_A));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A));
const caller = postgresRouter.createCaller(ctx());
await expect(
caller.move({ postgresId: "pg-1", targetEnvironmentId: "env-target" }),
).resolves.toMatchObject({ postgresId: "pg-1" });
expect(mockDb.update).toHaveBeenCalledOnce();
});
it("rejects moving a postgres that belongs to a foreign organization (source check)", async () => {
mockDb.query.postgres.findFirst.mockResolvedValue(pgRow(ORG_B));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A));
const caller = postgresRouter.createCaller(ctx());
await expect(
caller.move({ postgresId: "pg-1", targetEnvironmentId: "env-target" }),
).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "You are not authorized to move this postgres",
});
expect(mockDb.update).not.toHaveBeenCalled();
});
it("rejects moving to a target environment in a foreign organization (target check)", async () => {
mockDb.query.postgres.findFirst.mockResolvedValue(pgRow(ORG_A));
mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_B));
const caller = postgresRouter.createCaller(ctx());
await expect(
caller.move({ postgresId: "pg-1", targetEnvironmentId: "env-target" }),
).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
expect(mockDb.update).not.toHaveBeenCalled();
});
});

View File

@ -1018,6 +1018,30 @@ export const applicationRouter = createTRPCRouter({
service: ["create"],
});
const application = await findApplicationById(input.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this application",
});
}
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
const updatedApplication = await db
.update(applications)
.set({

View File

@ -786,6 +786,30 @@ export const composeRouter = createTRPCRouter({
service: ["create"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this compose",
});
}
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
const updatedCompose = await db
.update(composeTable)
.set({

View File

@ -433,6 +433,30 @@ export const libsqlRouter = createTRPCRouter({
service: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this libsql",
});
}
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
const updatedLibsql = await db
.update(libsqlTable)
.set({

View File

@ -460,6 +460,30 @@ export const mariadbRouter = createTRPCRouter({
service: ["create"],
});
const mariadb = await findMariadbById(input.mariadbId);
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mariadb",
});
}
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
const updatedMariadb = await db
.update(mariadbTable)
.set({

View File

@ -475,6 +475,30 @@ export const mongoRouter = createTRPCRouter({
service: ["create"],
});
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mongo",
});
}
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
const updatedMongo = await db
.update(mongoTable)
.set({

View File

@ -478,6 +478,30 @@ export const mysqlRouter = createTRPCRouter({
service: ["create"],
});
const mysql = await findMySqlById(input.mysqlId);
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mysql",
});
}
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
const updatedMysql = await db
.update(mysqlTable)
.set({

View File

@ -481,6 +481,30 @@ export const postgresRouter = createTRPCRouter({
service: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this postgres",
});
}
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
const updatedPostgres = await db
.update(postgresTable)
.set({

View File

@ -462,6 +462,30 @@ export const redisRouter = createTRPCRouter({
service: ["create"],
});
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this redis",
});
}
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
const updatedRedis = await db
.update(redisTable)
.set({

View File

@ -551,4 +551,4 @@ export const apiUpdateApplication = createSchema
.extend({
applicationId: z.string().min(1),
})
.omit({ serverId: true });
.omit({ serverId: true, environmentId: true });

View File

@ -264,7 +264,7 @@ export const apiUpdateCompose = createSchema
composeFile: z.string().optional(),
command: z.string().optional(),
})
.omit({ serverId: true });
.omit({ serverId: true, environmentId: true });
export const apiSaveEnvironmentVariablesCompose = createSchema
.pick({

View File

@ -246,7 +246,7 @@ export const apiUpdateLibsql = createSchema
.extend({
libsqlId: z.string().min(1),
})
.omit({ serverId: true });
.omit({ serverId: true, environmentId: true });
export const apiRebuildLibsql = createSchema
.pick({

View File

@ -220,7 +220,7 @@ export const apiUpdateMariaDB = createSchema
mariadbId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });
.omit({ serverId: true, environmentId: true });
export const apiRebuildMariadb = createSchema
.pick({

View File

@ -204,7 +204,7 @@ export const apiUpdateMongo = createSchema
dockerImage: z.string().optional(),
replicaSets: z.boolean().optional(),
})
.omit({ serverId: true });
.omit({ serverId: true, environmentId: true });
export const apiResetMongo = createSchema
.pick({

View File

@ -217,7 +217,7 @@ export const apiUpdateMySql = createSchema
mysqlId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });
.omit({ serverId: true, environmentId: true });
export const apiRebuildMysql = createSchema
.pick({

View File

@ -211,7 +211,7 @@ export const apiUpdatePostgres = createSchema
postgresId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });
.omit({ serverId: true, environmentId: true });
export const apiRebuildPostgres = createSchema
.pick({

View File

@ -198,7 +198,7 @@ export const apiUpdateRedis = createSchema
redisId: z.string().min(1),
dockerImage: z.string().optional(),
})
.omit({ serverId: true });
.omit({ serverId: true, environmentId: true });
export const apiRebuildRedis = createSchema
.pick({