diff --git a/apps/dokploy/__test__/docker/docker-router-security.test.ts b/apps/dokploy/__test__/docker/docker-router-security.test.ts index 5ad535b0960..e355ccebb40 100644 --- a/apps/dokploy/__test__/docker/docker-router-security.test.ts +++ b/apps/dokploy/__test__/docker/docker-router-security.test.ts @@ -4,12 +4,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ audit: vi.fn(), assertLocalDockerContainerAccess: vi.fn(), + assertLocalDockerContainerOrSystemAccess: vi.fn(), checkPermission: vi.fn(), + checkProtectedResourceAccess: vi.fn(), containerKill: vi.fn(), containerRemove: vi.fn(), containerRestart: vi.fn(), containerStart: vi.fn(), containerStop: vi.fn(), + deleteContainerFile: vi.fn(), findMemberByUserId: vi.fn(), findServerById: vi.fn(), getAccessibleServerIds: vi.fn(), @@ -17,9 +20,13 @@ const mocks = vi.hoisted(() => ({ getContainers: vi.fn(), getContainersByAppLabel: vi.fn(), getContainersByAppNameMatch: vi.fn(), + getDockerEvents: vi.fn(), getServiceContainersByAppName: vi.fn(), getStackContainersByAppName: vi.fn(), + listContainerFiles: vi.fn(), + readContainerFile: vi.fn(), uploadFileToContainer: vi.fn(), + writeContainerFile: vi.fn(), })); vi.mock("@dokploy/server", () => ({ @@ -28,15 +35,20 @@ vi.mock("@dokploy/server", () => ({ containerRestart: mocks.containerRestart, containerStart: mocks.containerStart, containerStop: mocks.containerStop, + deleteContainerFile: mocks.deleteContainerFile, findServerById: mocks.findServerById, getAccessibleServerIds: mocks.getAccessibleServerIds, getConfig: mocks.getConfig, getContainers: mocks.getContainers, getContainersByAppLabel: mocks.getContainersByAppLabel, getContainersByAppNameMatch: mocks.getContainersByAppNameMatch, + getDockerEvents: mocks.getDockerEvents, getServiceContainersByAppName: mocks.getServiceContainersByAppName, getStackContainersByAppName: mocks.getStackContainersByAppName, + listContainerFiles: mocks.listContainerFiles, + readContainerFile: mocks.readContainerFile, uploadFileToContainer: mocks.uploadFileToContainer, + writeContainerFile: mocks.writeContainerFile, })); vi.mock("@dokploy/server/services/permission", () => ({ @@ -50,11 +62,23 @@ vi.mock("@/server/api/utils/audit", () => ({ vi.mock("@/server/api/utils/local-docker-access", () => ({ assertLocalDockerContainerAccess: mocks.assertLocalDockerContainerAccess, + assertLocalDockerContainerOrSystemAccess: + mocks.assertLocalDockerContainerOrSystemAccess, +})); + +vi.mock("@dokploy/server/services/super-password", async (importOriginal) => ({ + ...(await importOriginal< + typeof import("@dokploy/server/services/super-password") + >()), + checkProtectedResourceAccess: mocks.checkProtectedResourceAccess, })); const { dockerRouter } = await import("../../server/api/routers/docker"); +const { getSuperSessionDenial } = await import( + "@dokploy/server/services/super-password" +); -const createCaller = () => +const createCaller = (authMethod: "session" | "api-key" = "session") => dockerRouter.createCaller({ db: {}, req: {}, @@ -62,6 +86,7 @@ const createCaller = () => session: { userId: "user-1", activeOrganizationId: "org-1", + ...(authMethod === "api-key" ? { authMethod } : {}), }, user: { id: "user-1", @@ -241,3 +266,298 @@ describe("docker router assigned-server boundary", () => { ); }); }); + +describe("docker router container file and event boundary", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.checkPermission.mockResolvedValue(undefined); + mocks.findMemberByUserId.mockResolvedValue({ role: "admin" }); + mocks.findServerById.mockResolvedValue({ + serverId: "server-1", + organizationId: "org-1", + }); + mocks.assertLocalDockerContainerAccess.mockResolvedValue({ + Id: "resolved-container-1", + Config: { + Labels: { + "com.docker.swarm.service.name": "app-1", + }, + }, + }); + mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-1"])); + mocks.listContainerFiles.mockResolvedValue([]); + mocks.readContainerFile.mockResolvedValue({ content: "" }); + mocks.getDockerEvents.mockResolvedValue([]); + mocks.assertLocalDockerContainerOrSystemAccess.mockImplementation( + (ctx, containerId, permission) => + mocks.assertLocalDockerContainerAccess(ctx, containerId, permission), + ); + mocks.checkProtectedResourceAccess.mockResolvedValue( + "super-session-required", + ); + }); + + const fileCalls = ( + caller: ReturnType, + serverId?: string, + ) => [ + { + name: "listContainerFiles", + run: () => + caller.listContainerFiles({ + containerId: "container-1", + path: "/", + serverId, + }), + sideEffect: mocks.listContainerFiles, + }, + { + name: "readContainerFile", + run: () => + caller.readContainerFile({ + containerId: "container-1", + path: "/etc/app.env", + serverId, + }), + sideEffect: mocks.readContainerFile, + }, + { + name: "writeContainerFile", + run: () => + caller.writeContainerFile({ + containerId: "container-1", + path: "/etc/app.env", + content: "KEY=value", + serverId, + }), + sideEffect: mocks.writeContainerFile, + }, + { + name: "deleteContainerFile", + run: () => + caller.deleteContainerFile({ + containerId: "container-1", + path: "/etc/app.env", + serverId, + }), + sideEffect: mocks.deleteContainerFile, + }, + ]; + + it("denies non-admin container file access before Docker side effects", async () => { + mocks.findMemberByUserId.mockResolvedValue({ role: "member" }); + + for (const call of fileCalls(createCaller())) { + await expect(call.run(), call.name).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + expect(mocks.audit).not.toHaveBeenCalled(); + }); + + it("denies container file access on inaccessible servers", async () => { + mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-2"])); + + for (const call of fileCalls(createCaller(), "server-1")) { + await expect(call.run(), call.name).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + expect(mocks.audit).not.toHaveBeenCalled(); + }); + + it("denies local container file access for containers outside the caller's services", async () => { + mocks.assertLocalDockerContainerAccess.mockRejectedValue( + new TRPCError({ code: "UNAUTHORIZED" }), + ); + + for (const call of fileCalls(createCaller())) { + await expect(call.run(), call.name).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + expect(mocks.audit).not.toHaveBeenCalled(); + }); + + it("binds local container file reads to the authorized container", async () => { + const caller = createCaller(); + + await caller.listContainerFiles({ containerId: "container-1", path: "/" }); + await caller.readContainerFile({ + containerId: "container-1", + path: "/etc/app.env", + }); + + expect(mocks.assertLocalDockerContainerAccess).toHaveBeenCalledWith( + expect.anything(), + "container-1", + "read", + ); + expect(mocks.listContainerFiles).toHaveBeenCalledWith( + "resolved-container-1", + "/", + undefined, + ); + expect(mocks.readContainerFile).toHaveBeenCalledWith( + "resolved-container-1", + "/etc/app.env", + undefined, + ); + }); + + it("requires docker.write for container file writes", async () => { + await createCaller().writeContainerFile({ + containerId: "container-1", + path: "/etc/app.env", + content: "KEY=value", + }); + + expect(mocks.checkPermission).toHaveBeenCalledWith(expect.anything(), { + docker: ["write"], + }); + expect(mocks.assertLocalDockerContainerAccess).toHaveBeenCalledWith( + expect.anything(), + "container-1", + "write", + ); + expect(mocks.writeContainerFile).toHaveBeenCalledWith( + "resolved-container-1", + "/etc/app.env", + "KEY=value", + undefined, + ); + }); + + it("requires docker.delete for container file deletes", async () => { + await createCaller().deleteContainerFile({ + containerId: "container-1", + path: "/etc/app.env", + }); + + expect(mocks.checkPermission).toHaveBeenCalledWith(expect.anything(), { + docker: ["delete"], + }); + expect(mocks.assertLocalDockerContainerAccess).toHaveBeenCalledWith( + expect.anything(), + "container-1", + "delete", + ); + expect(mocks.deleteContainerFile).toHaveBeenCalledWith( + "resolved-container-1", + "/etc/app.env", + undefined, + ); + }); + + describe("panel system containers", () => { + const systemContainer = { + Id: "system-container-1", + Config: { Labels: {} }, + }; + + beforeEach(() => { + mocks.assertLocalDockerContainerOrSystemAccess.mockImplementation( + async (_ctx, _containerId, _permission, assertSystemAccess) => { + await assertSystemAccess(); + return systemContainer; + }, + ); + }); + + it("denies system container files without an open super session", async () => { + for (const call of fileCalls(createCaller())) { + const error = await call.run().then( + () => null, + (caught: unknown) => caught, + ); + expect(error, call.name).toMatchObject({ code: "FORBIDDEN" }); + expect(getSuperSessionDenial(error), call.name).toBe( + "super-session-required", + ); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + expect(mocks.checkProtectedResourceAccess).toHaveBeenCalledWith({ + userId: "user-1", + viaApiKey: false, + }); + expect(mocks.audit).not.toHaveBeenCalled(); + }); + + it("opens system container files for owners and admins with an open super session", async () => { + mocks.checkProtectedResourceAccess.mockResolvedValue(null); + + for (const call of fileCalls(createCaller())) { + await call.run(); + expect(call.sideEffect, call.name).toHaveBeenCalledWith( + "system-container-1", + expect.anything(), + ...(call.name === "writeContainerFile" + ? [expect.anything(), undefined] + : [undefined]), + ); + } + }); + + it("keeps members out even with an open super session", async () => { + mocks.checkProtectedResourceAccess.mockResolvedValue(null); + mocks.findMemberByUserId.mockResolvedValue({ role: "member" }); + + for (const call of fileCalls(createCaller())) { + await expect(call.run(), call.name).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + }); + + it("never opens system container files through an API key", async () => { + mocks.checkProtectedResourceAccess.mockImplementation( + async ({ viaApiKey }: { viaApiKey: boolean }) => + viaApiKey ? "browser-session-required" : null, + ); + + for (const call of fileCalls(createCaller("api-key"))) { + await expect(call.run(), call.name).rejects.toMatchObject({ + code: "FORBIDDEN", + }); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + }); + }); + + it("denies non-admin Docker event reads", async () => { + mocks.findMemberByUserId.mockResolvedValue({ role: "member" }); + + await expect(createCaller().getEvents({})).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }); + + expect(mocks.getDockerEvents).not.toHaveBeenCalled(); + }); + + it("denies Docker event reads on inaccessible servers", async () => { + mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-2"])); + + await expect( + createCaller().getEvents({ serverId: "server-1" }), + ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + + expect(mocks.getDockerEvents).not.toHaveBeenCalled(); + }); + + it("checks admin role and server access before reading Docker events", async () => { + await expect( + createCaller().getEvents({ serverId: "server-1", minutes: 5 }), + ).resolves.toEqual([]); + + expect(mocks.findMemberByUserId).toHaveBeenCalledWith("user-1", "org-1"); + expect(mocks.getAccessibleServerIds).toHaveBeenCalledWith({ + userId: "user-1", + activeOrganizationId: "org-1", + }); + expect(mocks.getDockerEvents).toHaveBeenCalledWith("server-1", 5); + }); +}); diff --git a/apps/dokploy/__test__/docker/docker-volume-router-security.test.ts b/apps/dokploy/__test__/docker/docker-volume-router-security.test.ts new file mode 100644 index 00000000000..39386380737 --- /dev/null +++ b/apps/dokploy/__test__/docker/docker-volume-router-security.test.ts @@ -0,0 +1,253 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + audit: vi.fn(), + assertLocalDockerContainerAccess: vi.fn(), + checkPermission: vi.fn(), + checkProtectedResourceAccess: vi.fn(), + deleteVolumeFile: vi.fn(), + isLocalSystemDockerVolume: vi.fn(), + findMemberByUserId: vi.fn(), + findServerById: vi.fn(), + getAccessibleServerIds: vi.fn(), + getVolumeConfig: vi.fn(), + getVolumes: vi.fn(), + getVolumesSize: vi.fn(), + listVolumeFiles: vi.fn(), + readVolumeFile: vi.fn(), + removeVolume: vi.fn(), + writeVolumeFile: vi.fn(), +})); + +vi.mock("@dokploy/server", () => ({ + deleteVolumeFile: mocks.deleteVolumeFile, + findServerById: mocks.findServerById, + getAccessibleServerIds: mocks.getAccessibleServerIds, + getVolumeConfig: mocks.getVolumeConfig, + getVolumes: mocks.getVolumes, + getVolumesSize: mocks.getVolumesSize, + listVolumeFiles: mocks.listVolumeFiles, + readVolumeFile: mocks.readVolumeFile, + removeVolume: mocks.removeVolume, + writeVolumeFile: mocks.writeVolumeFile, +})); + +vi.mock("@dokploy/server/services/permission", () => ({ + checkPermission: mocks.checkPermission, + findMemberByUserId: mocks.findMemberByUserId, +})); + +vi.mock("@/server/api/utils/audit", () => ({ + audit: mocks.audit, +})); + +vi.mock("@/server/api/utils/local-docker-access", () => ({ + assertLocalDockerContainerAccess: mocks.assertLocalDockerContainerAccess, + isLocalSystemDockerVolume: mocks.isLocalSystemDockerVolume, +})); + +vi.mock("@dokploy/server/services/super-password", async (importOriginal) => ({ + ...(await importOriginal< + typeof import("@dokploy/server/services/super-password") + >()), + checkProtectedResourceAccess: mocks.checkProtectedResourceAccess, +})); + +const { dockerVolumeRouter } = await import( + "../../server/api/routers/docker-volume" +); + +const createCaller = () => + dockerVolumeRouter.createCaller({ + db: {}, + req: {}, + res: {}, + session: { + userId: "user-1", + activeOrganizationId: "org-1", + }, + user: { + id: "user-1", + role: "member", + }, + } as never); + +const volumeCalls = (serverId?: string) => { + const caller = createCaller(); + return [ + { + name: "listVolumeFiles", + run: () => + caller.listVolumeFiles({ volumeName: "data", path: "/", serverId }), + sideEffect: mocks.listVolumeFiles, + }, + { + name: "readVolumeFile", + run: () => + caller.readVolumeFile({ + volumeName: "data", + path: "/app.env", + serverId, + }), + sideEffect: mocks.readVolumeFile, + }, + { + name: "writeVolumeFile", + run: () => + caller.writeVolumeFile({ + volumeName: "data", + path: "/app.env", + content: "KEY=value", + serverId, + }), + sideEffect: mocks.writeVolumeFile, + }, + { + name: "deleteVolumeFile", + run: () => + caller.deleteVolumeFile({ + volumeName: "data", + path: "/app.env", + serverId, + }), + sideEffect: mocks.deleteVolumeFile, + }, + { + name: "removeVolume", + run: () => caller.removeVolume({ volumeName: "data", serverId }), + sideEffect: mocks.removeVolume, + }, + ]; +}; + +describe("docker volume router access boundary", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.checkPermission.mockResolvedValue(undefined); + mocks.findMemberByUserId.mockResolvedValue({ role: "admin" }); + mocks.findServerById.mockResolvedValue({ + serverId: "server-1", + organizationId: "org-1", + }); + mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-1"])); + mocks.listVolumeFiles.mockResolvedValue([]); + mocks.readVolumeFile.mockResolvedValue({ content: "" }); + mocks.isLocalSystemDockerVolume.mockResolvedValue(false); + mocks.checkProtectedResourceAccess.mockResolvedValue( + "super-session-required", + ); + }); + + it("denies non-admin volume file access and removal on the local host", async () => { + mocks.findMemberByUserId.mockResolvedValue({ role: "member" }); + + for (const call of volumeCalls()) { + await expect(call.run(), call.name).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + expect(mocks.audit).not.toHaveBeenCalled(); + }); + + it("denies non-admin volume file access on remote servers", async () => { + mocks.findMemberByUserId.mockResolvedValue({ role: "member" }); + + for (const call of volumeCalls("server-1")) { + await expect(call.run(), call.name).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + }); + + it("denies volume file access on inaccessible servers", async () => { + mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-2"])); + + for (const call of volumeCalls("server-1")) { + await expect(call.run(), call.name).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + expect(mocks.audit).not.toHaveBeenCalled(); + }); + + it("requires docker.write for writes and docker.delete for deletes", async () => { + const caller = createCaller(); + + await caller.writeVolumeFile({ + volumeName: "data", + path: "/app.env", + content: "KEY=value", + }); + expect(mocks.checkPermission).toHaveBeenLastCalledWith(expect.anything(), { + docker: ["write"], + }); + + await caller.deleteVolumeFile({ volumeName: "data", path: "/app.env" }); + expect(mocks.checkPermission).toHaveBeenLastCalledWith(expect.anything(), { + docker: ["delete"], + }); + + await caller.removeVolume({ volumeName: "data" }); + expect(mocks.checkPermission).toHaveBeenLastCalledWith(expect.anything(), { + docker: ["delete"], + }); + }); + + it("checks admin role and server access before browsing volumes", async () => { + await expect( + createCaller().listVolumeFiles({ + volumeName: "data", + path: "/", + serverId: "server-1", + }), + ).resolves.toEqual([]); + + expect(mocks.findMemberByUserId).toHaveBeenCalledWith("user-1", "org-1"); + expect(mocks.getAccessibleServerIds).toHaveBeenCalledWith({ + userId: "user-1", + activeOrganizationId: "org-1", + }); + expect(mocks.listVolumeFiles).toHaveBeenCalledWith("data", "/", "server-1"); + }); + + describe("panel system volumes", () => { + const fileCalls = () => + volumeCalls().filter((call) => call.name !== "removeVolume"); + + beforeEach(() => { + mocks.isLocalSystemDockerVolume.mockResolvedValue(true); + }); + + it("denies local system volume files without an open super session", async () => { + for (const call of fileCalls()) { + await expect(call.run(), call.name).rejects.toMatchObject({ + code: "FORBIDDEN", + }); + expect(call.sideEffect, call.name).not.toHaveBeenCalled(); + } + expect(mocks.isLocalSystemDockerVolume).toHaveBeenCalledWith("data"); + }); + + it("opens local system volume files with an open super session", async () => { + mocks.checkProtectedResourceAccess.mockResolvedValue(null); + + for (const call of fileCalls()) { + await call.run(); + expect(call.sideEffect, call.name).toHaveBeenCalled(); + } + }); + + it("does not inspect remote volumes for panel containers", async () => { + await createCaller().listVolumeFiles({ + volumeName: "data", + path: "/", + serverId: "server-1", + }); + expect(mocks.isLocalSystemDockerVolume).not.toHaveBeenCalled(); + expect(mocks.listVolumeFiles).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/dokploy/__test__/docker/local-docker-system-access.test.ts b/apps/dokploy/__test__/docker/local-docker-system-access.test.ts new file mode 100644 index 00000000000..aa965e8c9ea --- /dev/null +++ b/apps/dokploy/__test__/docker/local-docker-system-access.test.ts @@ -0,0 +1,154 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + checkServicePermissionAndAccess: vi.fn(), + execAsync: vi.fn(), + getConfig: vi.fn(), + findFirst: {} as Record>, +})); + +vi.mock("@dokploy/server", () => ({ + execAsync: mocks.execAsync, + getConfig: mocks.getConfig, +})); + +vi.mock("@dokploy/server/db", () => ({ + db: { + query: new Proxy( + {}, + { + get: (_target, table: string) => { + mocks.findFirst[table] ??= vi.fn(async () => undefined); + return { findFirst: mocks.findFirst[table] }; + }, + }, + ), + }, +})); + +vi.mock("@dokploy/server/services/permission", () => ({ + checkServicePermissionAndAccess: mocks.checkServicePermissionAndAccess, +})); + +const { assertLocalDockerContainerOrSystemAccess, isLocalSystemDockerVolume } = + await import("@/server/api/utils/local-docker-access"); + +const ctx = { + user: { id: "user-1" }, + session: { activeOrganizationId: "org-1" }, +}; + +const serviceContainer = { + Id: "service-container", + Config: { Labels: { "com.docker.swarm.service.name": "web-app-1" } }, +}; +const panelContainer = { + Id: "panel-container", + Config: { Labels: { "com.docker.swarm.service.name": "dokploy-postgres" } }, +}; +const unlabelledContainer = { Id: "traefik-container", Config: { Labels: {} } }; + +describe("local Docker system containers", () => { + beforeEach(() => { + vi.clearAllMocks(); + for (const findFirst of Object.values(mocks.findFirst)) { + findFirst.mockResolvedValue(undefined); + } + mocks.findFirst.applications ??= vi.fn(); + mocks.findFirst.applications.mockImplementation(async () => undefined); + }); + + it("checks service access for containers that belong to a service", async () => { + mocks.getConfig.mockResolvedValue(serviceContainer); + mocks.findFirst.applications?.mockResolvedValue({ + applicationId: "app-1", + serverId: null, + }); + const assertSystemAccess = vi.fn(); + + await expect( + assertLocalDockerContainerOrSystemAccess( + ctx, + "service-container", + "read", + assertSystemAccess, + ), + ).resolves.toBe(serviceContainer); + + expect(mocks.checkServicePermissionAndAccess).toHaveBeenCalledWith( + ctx, + "app-1", + { docker: ["read"] }, + ); + expect(assertSystemAccess).not.toHaveBeenCalled(); + }); + + it("routes containers without a service through the system access check", async () => { + for (const container of [panelContainer, unlabelledContainer]) { + mocks.getConfig.mockResolvedValue(container); + const assertSystemAccess = vi + .fn() + .mockRejectedValue(new Error("super session required")); + + await expect( + assertLocalDockerContainerOrSystemAccess( + ctx, + container.Id, + "write", + assertSystemAccess, + ), + ).rejects.toThrow("super session required"); + expect(assertSystemAccess).toHaveBeenCalledTimes(1); + + assertSystemAccess.mockResolvedValue(undefined); + await expect( + assertLocalDockerContainerOrSystemAccess( + ctx, + container.Id, + "write", + assertSystemAccess, + ), + ).resolves.toBe(container); + } + expect(mocks.checkServicePermissionAndAccess).not.toHaveBeenCalled(); + }); + + it("flags volumes mounted by the panel's own containers", async () => { + mocks.execAsync.mockResolvedValue({ + stdout: "service-container\npanel-container\n", + }); + mocks.getConfig.mockImplementation(async (id: string) => + id === "panel-container" ? panelContainer : serviceContainer, + ); + mocks.findFirst.applications?.mockResolvedValueOnce({ + applicationId: "app-1", + serverId: null, + }); + mocks.findFirst.applications?.mockResolvedValueOnce(undefined); + + await expect(isLocalSystemDockerVolume("dokploy-postgres")).resolves.toBe( + true, + ); + expect(mocks.execAsync).toHaveBeenCalledWith( + "docker ps -a --no-trunc --filter volume\\=dokploy-postgres --format '{{.ID}}'", + ); + }); + + it("does not flag volumes used only by service containers", async () => { + mocks.execAsync.mockResolvedValue({ stdout: "service-container\n" }); + mocks.getConfig.mockResolvedValue(serviceContainer); + mocks.findFirst.applications?.mockResolvedValue({ + applicationId: "app-1", + serverId: null, + }); + + await expect(isLocalSystemDockerVolume("app-data")).resolves.toBe(false); + }); + + it("rejects volume names that are not Docker identifiers", async () => { + await expect(isLocalSystemDockerVolume("data;rm -rf /")).rejects.toThrow( + "Invalid Docker resource identifier", + ); + expect(mocks.execAsync).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/dokploy/__test__/helpers/fake-drizzle-db.ts b/apps/dokploy/__test__/helpers/fake-drizzle-db.ts new file mode 100644 index 00000000000..b873a63b6cd --- /dev/null +++ b/apps/dokploy/__test__/helpers/fake-drizzle-db.ts @@ -0,0 +1,300 @@ +import { + Column, + getTableColumns, + getTableName, + is, + Param, + SQL, + StringChunk, + type Table, +} from "drizzle-orm"; + +type Row = Record; + +const isBlankChunk = (chunk: unknown) => + is(chunk, StringChunk) && chunk.value.join("").trim() === ""; + +const chunkText = (chunk: unknown) => + is(chunk, StringChunk) ? chunk.value.join("").trim() : null; + +const comparable = (value: unknown) => + value instanceof Date ? value.getTime() : value; + +/** + * In-memory stand-in for the subset of drizzle used by the super password + * service: relational findFirst/findMany, insert (with onConflictDoUpdate), + * update ... returning and delete, with eq/ne/gt/lt/isNull/isNotNull/and/or + * conditions evaluated against stored rows. + */ +export const createFakeDrizzleDb = (tables: Record) => { + const rows = new Map(); + const columnKeys = new Map(); + const tableByName = new Map(); + + for (const table of Object.values(tables)) { + rows.set(table, []); + tableByName.set(getTableName(table), table); + for (const [key, column] of Object.entries(getTableColumns(table))) { + columnKeys.set(column, key); + } + } + + const keyOf = (column: unknown) => { + const key = columnKeys.get(column); + if (!key) { + throw new Error("Unknown column in fake db condition"); + } + return key; + }; + + const evaluate = (condition: unknown, row: Row): boolean => { + if (!condition) { + return true; + } + if (!is(condition, SQL)) { + throw new Error("Unsupported condition in fake db"); + } + return evaluateParts( + condition.queryChunks.filter((chunk) => !isBlankChunk(chunk)), + row, + ); + }; + + const evaluateParts = (parts: unknown[], row: Row): boolean => { + if (chunkText(parts[0]) === "(" && chunkText(parts.at(-1)) === ")") { + return evaluateParts(parts.slice(1, -1), row); + } + + if (parts.every((part) => is(part, SQL) || chunkText(part) !== null)) { + const operands = parts.filter((part) => is(part, SQL)); + const operator = parts.map(chunkText).find((text) => text) ?? "and"; + const results = operands.map((operand) => evaluate(operand, row)); + return operator === "or" ? results.some(Boolean) : results.every(Boolean); + } + + if (!is(parts[0], Column)) { + throw new Error("Unsupported condition shape in fake db"); + } + const left = comparable(row[keyOf(parts[0])] ?? null); + const operator = chunkText(parts[1]); + if (operator === "is null") { + return left === null; + } + if (operator === "is not null") { + return left !== null; + } + const raw = parts[2]; + const right = comparable(is(raw, Param) ? raw.value : raw); + switch (operator) { + case "=": + return left === right; + case "<>": + return left !== right; + case ">": + return left !== null && (left as number) > (right as number); + case "<": + return left !== null && (left as number) < (right as number); + case ">=": + return left !== null && (left as number) >= (right as number); + case "<=": + return left !== null && (left as number) <= (right as number); + default: + throw new Error(`Unsupported operator in fake db: ${operator}`); + } + }; + + const resolveValue = (value: unknown, row: Row) => { + if (!is(value, SQL)) { + return value; + } + const parts = value.queryChunks.filter((chunk) => !isBlankChunk(chunk)); + const match = /^\+\s*(\d+)$/.exec(chunkText(parts[1]) ?? ""); + if (is(parts[0], Column) && match) { + return Number(row[keyOf(parts[0])] ?? 0) + Number(match[1]); + } + throw new Error("Unsupported SQL value in fake db"); + }; + + const withDefaults = (table: Table, values: Row) => { + const row: Row = {}; + for (const [key, column] of Object.entries(getTableColumns(table))) { + if (values[key] !== undefined) { + row[key] = values[key]; + } else if (column.defaultFn) { + row[key] = column.defaultFn(); + } else if (is(column.default, SQL)) { + row[key] = new Date(); + } else if (column.default !== undefined) { + row[key] = column.default; + } else { + row[key] = null; + } + } + return row; + }; + + const primaryKeyOf = (table: Table) => + Object.entries(getTableColumns(table)).find( + ([, column]) => column.primary, + )?.[0]; + + const project = (row: Row, columns?: Record) => { + if (!columns) { + return { ...row }; + } + return Object.fromEntries( + Object.entries(columns).map(([alias, column]) => [ + alias, + row[keyOf(column)], + ]), + ); + }; + + const insertRow = (table: Table, values: Row) => { + const list = rows.get(table) ?? []; + const row = withDefaults(table, values); + const primaryKey = primaryKeyOf(table); + if ( + primaryKey && + list.some((existing) => existing[primaryKey] === row[primaryKey]) + ) { + throw new Error(`duplicate key in ${getTableName(table)}`); + } + list.push(row); + rows.set(table, list); + return row; + }; + + const lazy = (run: () => T, extra: Record = {}) => ({ + ...extra, + // biome-ignore lint/suspicious/noThenProperty: emulates drizzle's thenable query builders. + then: ( + resolve: (value: T) => unknown, + reject: (error: unknown) => unknown, + ) => { + try { + return Promise.resolve(resolve(run())); + } catch (error) { + return Promise.resolve(reject(error)); + } + }, + }); + + // Only one-to-one relations named after a registered table whose id column + // is `Id` (notification.email / notification.resend). + const withRelations = (row: Row, relations?: Record) => { + const result: Row = { ...row }; + for (const name of Object.keys(relations ?? {})) { + const relatedTable = tables[name]; + const foreignKey = `${name}Id`; + const related = relatedTable + ? (rows.get(relatedTable) ?? []).find( + (candidate) => + row[foreignKey] !== null && + candidate[foreignKey] === row[foreignKey], + ) + : undefined; + result[name] = related ? { ...related } : null; + } + return result; + }; + + const queryApi = (table: Table) => ({ + findFirst: async ( + options: { where?: unknown; with?: Record } = {}, + ) => { + const found = (rows.get(table) ?? []).find((row) => + evaluate(options.where, row), + ); + return found ? withRelations(found, options.with) : undefined; + }, + findMany: async ( + options: { where?: unknown; with?: Record } = {}, + ) => + (rows.get(table) ?? []) + .filter((row) => evaluate(options.where, row)) + .map((row) => withRelations(row, options.with)), + }); + + const query = Object.fromEntries( + Object.entries(tables).map(([key, table]) => [key, queryApi(table)]), + ); + + const db: Record = { + query, + transaction: async (run: (tx: unknown) => Promise) => run(db), + insert: (table: Table) => ({ + values: (values: Row) => + lazy(() => [insertRow(table, values)], { + returning: async (columns?: Record) => [ + project(insertRow(table, values), columns), + ], + onConflictDoUpdate: async ({ set }: { set: Row }) => { + const primaryKey = primaryKeyOf(table); + const existing = (rows.get(table) ?? []).find( + (row) => primaryKey && row[primaryKey] === values[primaryKey], + ); + if (existing) { + for (const [key, value] of Object.entries(set)) { + existing[key] = resolveValue(value, existing); + } + return [existing]; + } + return [insertRow(table, values)]; + }, + }), + }), + update: (table: Table) => ({ + set: (values: Row) => ({ + where: (condition: unknown) => { + const apply = () => { + const updated: Row[] = []; + for (const row of rows.get(table) ?? []) { + if (evaluate(condition, row)) { + for (const [key, value] of Object.entries(values)) { + if (value !== undefined) { + row[key] = resolveValue(value, row); + } + } + updated.push(row); + } + } + return updated; + }; + return lazy(apply, { + returning: async (columns?: Record) => + apply().map((row) => project(row, columns)), + }); + }, + }), + }), + delete: (table: Table) => ({ + where: (condition: unknown) => { + const remove = () => { + const list = rows.get(table) ?? []; + rows.set( + table, + list.filter((row) => !evaluate(condition, row)), + ); + return list.filter((row) => evaluate(condition, row)); + }; + return lazy(remove, { + returning: async (columns?: Record) => + remove().map((row) => project(row, columns)), + }); + }, + }), + }; + + return { + db, + rows: (table: Table) => rows.get(table) ?? [], + seed: (table: Table, values: Row) => insertRow(table, values), + reset: () => { + for (const table of rows.keys()) { + rows.set(table, []); + } + }, + tableByName, + }; +}; diff --git a/apps/dokploy/__test__/logs/read-logs-permission.test.ts b/apps/dokploy/__test__/logs/read-logs-permission.test.ts new file mode 100644 index 00000000000..66499018b14 --- /dev/null +++ b/apps/dokploy/__test__/logs/read-logs-permission.test.ts @@ -0,0 +1,343 @@ +import { ac, memberRole } from "@dokploy/server/lib/access-control"; +import { TRPCError } from "@trpc/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const serverMocks = vi.hoisted(() => ({ + findApplicationById: vi.fn(), + findComposeById: vi.fn(), + findLibsqlById: vi.fn(), + findMariadbById: vi.fn(), + findMongoById: vi.fn(), + findMySqlById: vi.fn(), + findPostgresById: vi.fn(), + findRedisById: vi.fn(), + getContainerLogs: vi.fn(), + getContainersByAppNameMatch: vi.fn(), + noop: vi.fn(), +})); + +const permissionMocks = vi.hoisted(() => ({ + activeRole: { current: null as null | { authorize: (p: never) => unknown } }, + checkServiceAccess: vi.fn(), + checkServicePermissionAndAccess: vi.fn(), +})); + +vi.mock("@dokploy/server", () => ({ + IS_CLOUD: false, + addDomainToCompose: serverMocks.noop, + checkPortInUse: serverMocks.noop, + clearOldDeployments: serverMocks.noop, + cloneCompose: serverMocks.noop, + createApplication: serverMocks.noop, + createCommand: serverMocks.noop, + createCompose: serverMocks.noop, + createComposeByTemplate: serverMocks.noop, + createDomain: serverMocks.noop, + createLibsql: serverMocks.noop, + createMariadb: serverMocks.noop, + createMongo: serverMocks.noop, + createMount: serverMocks.noop, + createMysql: serverMocks.noop, + createPostgres: serverMocks.noop, + createRedis: serverMocks.noop, + deleteAllMiddlewares: serverMocks.noop, + deleteMount: serverMocks.noop, + deployLibsql: serverMocks.noop, + deployMariadb: serverMocks.noop, + deployMongo: serverMocks.noop, + deployMySql: serverMocks.noop, + deployPostgres: serverMocks.noop, + deployRedis: serverMocks.noop, + execAsync: serverMocks.noop, + execAsyncRemote: serverMocks.noop, + findApplicationById: serverMocks.findApplicationById, + findBackupsByDbId: serverMocks.noop, + findComposeById: serverMocks.findComposeById, + findDomainsByComposeId: serverMocks.noop, + findEnvironmentById: serverMocks.noop, + findLibsqlById: serverMocks.findLibsqlById, + findMariadbById: serverMocks.findMariadbById, + findMongoById: serverMocks.findMongoById, + findMySqlById: serverMocks.findMySqlById, + findPostgresById: serverMocks.findPostgresById, + findProjectById: serverMocks.noop, + findRedisById: serverMocks.findRedisById, + findRegistryById: serverMocks.noop, + findServerById: serverMocks.noop, + getAccessibleServerIds: serverMocks.noop, + getApplicationStats: serverMocks.noop, + getComposeContainer: serverMocks.noop, + getContainerLogs: serverMocks.getContainerLogs, + getContainersByAppNameMatch: serverMocks.getContainersByAppNameMatch, + getMountPath: serverMocks.noop, + getServiceContainerCommand: serverMocks.noop, + getWebServerSettings: serverMocks.noop, + loadServices: serverMocks.noop, + mechanizeDockerContainer: serverMocks.noop, + randomizeComposeFile: serverMocks.noop, + randomizeIsolatedDeploymentComposeFile: serverMocks.noop, + readConfig: serverMocks.noop, + readRemoteConfig: serverMocks.noop, + rebuildDatabase: serverMocks.noop, + removeCompose: serverMocks.noop, + removeComposeDirectory: serverMocks.noop, + removeDeployments: serverMocks.noop, + removeDeploymentsByComposeId: serverMocks.noop, + removeDirectoryCode: serverMocks.noop, + removeDomainById: serverMocks.noop, + removeLibsqlById: serverMocks.noop, + removeMariadbById: serverMocks.noop, + removeMongoById: serverMocks.noop, + removeMonitoringDirectory: serverMocks.noop, + removeMySqlById: serverMocks.noop, + removePostgresById: serverMocks.noop, + removeRedisById: serverMocks.noop, + removeService: serverMocks.noop, + removeTraefikConfig: serverMocks.noop, + startCompose: serverMocks.noop, + startService: serverMocks.noop, + startServiceRemote: serverMocks.noop, + stopCompose: serverMocks.noop, + stopService: serverMocks.noop, + stopServiceRemote: serverMocks.noop, + unzipDrop: serverMocks.noop, + updateApplication: serverMocks.noop, + updateApplicationStatus: serverMocks.noop, + updateCompose: serverMocks.noop, + updateDeploymentStatus: serverMocks.noop, + updateLibsqlById: serverMocks.noop, + updateMariadbById: serverMocks.noop, + updateMongoById: serverMocks.noop, + updateMySqlById: serverMocks.noop, + updatePostgresById: serverMocks.noop, + updateRedisById: serverMocks.noop, + upsertApplicationEnvironment: serverMocks.noop, + writeConfig: serverMocks.noop, + writeConfigRemote: serverMocks.noop, +})); + +vi.mock("@dokploy/server/db", () => ({ + db: {}, +})); + +vi.mock("@dokploy/server/services/git-provider", () => ({ + canEditDeployGitSource: vi.fn(), + redactGitProviderSecrets: vi.fn((value) => value), +})); + +vi.mock("@dokploy/server/services/permission", () => ({ + addNewService: vi.fn(), + checkPermission: vi.fn(), + checkServiceAccess: permissionMocks.checkServiceAccess, + checkServicePermissionAndAccess: + permissionMocks.checkServicePermissionAndAccess, + findMemberByUserId: vi.fn(), + hasPermission: vi.fn(), +})); + +vi.mock("@dokploy/server/templates/github", () => ({ + fetchTemplateFiles: vi.fn(), + fetchTemplatesList: vi.fn(), +})); + +vi.mock("@dokploy/server/templates/processors", () => ({ + processTemplate: vi.fn(), +})); + +vi.mock("@/server/api/utils/audit", () => ({ + audit: vi.fn(), +})); + +vi.mock("@/server/db", () => ({ + db: {}, +})); + +vi.mock("@/server/queues/queueSetup", () => ({ + cleanQueuesByApplication: vi.fn(), + cleanQueuesByCompose: vi.fn(), + killDockerBuild: vi.fn(), + myQueue: { + add: vi.fn(), + getJob: vi.fn(), + remove: vi.fn(), + }, +})); + +vi.mock("@/server/utils/deploy", () => ({ + cancelDeployment: vi.fn(), + deploy: vi.fn(), +})); + +const { applicationRouter } = await import( + "../../server/api/routers/application" +); +const { composeRouter } = await import("../../server/api/routers/compose"); +const { libsqlRouter } = await import("../../server/api/routers/libsql"); +const { mariadbRouter } = await import("../../server/api/routers/mariadb"); +const { mongoRouter } = await import("../../server/api/routers/mongo"); +const { mysqlRouter } = await import("../../server/api/routers/mysql"); +const { postgresRouter } = await import("../../server/api/routers/postgres"); +const { redisRouter } = await import("../../server/api/routers/redis"); + +const createContext = () => + ({ + db: {}, + req: {}, + res: {}, + session: { + userId: "user-1", + activeOrganizationId: "org-1", + }, + user: { + id: "user-1", + role: "member", + }, + }) as never; + +const serviceRecord = (idField: string, id: string) => ({ + [idField]: id, + appName: `${id}-app`, + composeType: "docker-compose", + serverId: null, + environment: { + project: { + organizationId: "org-1", + }, + }, +}); + +type ReadLogsCaller = { + readLogs: (input: Record) => Promise; +}; + +const cases = [ + { + name: "application", + id: "app-1", + idField: "applicationId", + find: serverMocks.findApplicationById, + caller: () => + applicationRouter.createCaller( + createContext(), + ) as unknown as ReadLogsCaller, + }, + { + name: "compose", + id: "compose-1", + idField: "composeId", + find: serverMocks.findComposeById, + caller: () => + composeRouter.createCaller(createContext()) as unknown as ReadLogsCaller, + extraInput: { containerId: "compose-container-1" }, + }, + { + name: "postgres", + id: "postgres-1", + idField: "postgresId", + find: serverMocks.findPostgresById, + caller: () => + postgresRouter.createCaller(createContext()) as unknown as ReadLogsCaller, + }, + { + name: "mysql", + id: "mysql-1", + idField: "mysqlId", + find: serverMocks.findMySqlById, + caller: () => + mysqlRouter.createCaller(createContext()) as unknown as ReadLogsCaller, + }, + { + name: "mariadb", + id: "mariadb-1", + idField: "mariadbId", + find: serverMocks.findMariadbById, + caller: () => + mariadbRouter.createCaller(createContext()) as unknown as ReadLogsCaller, + }, + { + name: "mongo", + id: "mongo-1", + idField: "mongoId", + find: serverMocks.findMongoById, + caller: () => + mongoRouter.createCaller(createContext()) as unknown as ReadLogsCaller, + }, + { + name: "redis", + id: "redis-1", + idField: "redisId", + find: serverMocks.findRedisById, + caller: () => + redisRouter.createCaller(createContext()) as unknown as ReadLogsCaller, + }, + { + name: "libsql", + id: "libsql-1", + idField: "libsqlId", + find: serverMocks.findLibsqlById, + caller: () => + libsqlRouter.createCaller(createContext()) as unknown as ReadLogsCaller, + }, +]; + +const roleWithoutLogs = ac.newRole({ service: ["read"], logs: [] }); + +describe("service readLogs permission boundary", () => { + beforeEach(() => { + vi.clearAllMocks(); + permissionMocks.activeRole.current = null; + const authorizeWithActiveRole = async ( + _ctx: unknown, + _serviceId: string, + permissions: Record, + ) => { + const result = permissionMocks.activeRole.current?.authorize( + permissions as never, + ) as { success: boolean } | undefined; + if (!result?.success) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + }; + permissionMocks.checkServiceAccess.mockImplementation( + (ctx: unknown, serviceId: string, action = "read") => + authorizeWithActiveRole(ctx, serviceId, { service: [action] }), + ); + permissionMocks.checkServicePermissionAndAccess.mockImplementation( + authorizeWithActiveRole, + ); + serverMocks.getContainerLogs.mockResolvedValue("log line"); + serverMocks.getContainersByAppNameMatch.mockResolvedValue([ + { containerId: "compose-container-1" }, + ]); + for (const testCase of cases) { + testCase.find.mockResolvedValue( + serviceRecord(testCase.idField, testCase.id), + ); + } + }); + + it.each(cases)( + "$name readLogs requires logs.read and keeps member access", + async (testCase) => { + const input = { + [testCase.idField]: testCase.id, + ...(testCase.extraInput ?? {}), + }; + + permissionMocks.activeRole.current = roleWithoutLogs; + await expect(testCase.caller().readLogs(input)).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }); + expect(serverMocks.getContainerLogs).not.toHaveBeenCalled(); + + permissionMocks.activeRole.current = memberRole; + await expect(testCase.caller().readLogs(input)).resolves.toBe("log line"); + expect( + permissionMocks.checkServicePermissionAndAccess, + ).toHaveBeenLastCalledWith(expect.anything(), testCase.id, { + service: ["read"], + logs: ["read"], + }); + }, + ); +}); diff --git a/apps/dokploy/__test__/security/super-password-alerts.test.ts b/apps/dokploy/__test__/security/super-password-alerts.test.ts new file mode 100644 index 00000000000..07f04fb6218 --- /dev/null +++ b/apps/dokploy/__test__/security/super-password-alerts.test.ts @@ -0,0 +1,590 @@ +import bcrypt from "bcrypt"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + audit: vi.fn(), + senders: { + sendCustomNotification: vi.fn(), + sendDiscordNotification: vi.fn(), + sendEmailNotification: vi.fn(), + sendGotifyNotification: vi.fn(), + sendLarkNotification: vi.fn(), + sendMattermostNotification: vi.fn(), + sendNtfyNotification: vi.fn(), + sendPushoverNotification: vi.fn(), + sendResendNotification: vi.fn(), + sendSlackNotification: vi.fn(), + sendTeamsNotification: vi.fn(), + sendTelegramNotification: vi.fn(), + }, +})); + +vi.mock("@dokploy/server/db", async () => { + const schema = await import("@dokploy/server/db/schema"); + const { createFakeDrizzleDb } = await import("../helpers/fake-drizzle-db"); + const fake = createFakeDrizzleDb({ + superPassword: schema.superPassword, + superSession: schema.superSession, + superPasswordToken: schema.superPasswordToken, + account: schema.account, + member: schema.member, + user: schema.user, + notifications: schema.notifications, + slack: schema.slack, + telegram: schema.telegram, + discord: schema.discord, + email: schema.email, + resend: schema.resend, + gotify: schema.gotify, + ntfy: schema.ntfy, + mattermost: schema.mattermost, + custom: schema.custom, + lark: schema.lark, + pushover: schema.pushover, + teams: schema.teams, + }); + return { db: fake.db, fake }; +}); + +vi.mock("@dokploy/server/utils/notifications/utils", () => mocks.senders); + +vi.mock("@dokploy/server/services/admin", () => ({ + getDokployUrl: async () => "https://panel.example.test", + getTrustedOrigins: async () => [], + getUserByToken: vi.fn(), +})); + +vi.mock("@/server/api/utils/audit", () => ({ audit: mocks.audit })); + +const schema = await import("@dokploy/server/db/schema"); +const { fake } = (await import("@dokploy/server/db")) as unknown as { + fake: ReturnType< + typeof import("../helpers/fake-drizzle-db")["createFakeDrizzleDb"] + >; +}; +const service = await import("@dokploy/server/services/super-password"); +const { createTRPCRouter } = await import("@/server/api/trpc"); +const { superPasswordRouter } = await import( + "@/server/api/routers/super-password" +); +const { notificationRouter } = await import( + "@/server/api/routers/notification" +); + +const router = createTRPCRouter({ + superPassword: superPasswordRouter, + notification: notificationRouter, +}); + +const owner = { + id: "user-1", + email: "owner@example.test", + role: "owner" as const, + ownerId: "user-1", +}; +const SUPER_PASSWORD = "super-password-1"; +const LOGIN_PASSWORD = "login-password-1"; +const HINT = "secret-hint-value"; + +const browser = () => + router.createCaller({ + db: {}, + req: { + headers: { "user-agent": "Browser/1.0", "x-real-ip": "203.0.113.7" }, + }, + res: {}, + user: owner, + session: { + id: "session-1", + userId: owner.id, + activeOrganizationId: "org-1", + authMethod: "session", + }, + } as never); + +const seedOwner = async () => { + fake.seed(schema.member, { + id: "member-1", + organizationId: "org-1", + userId: owner.id, + role: "owner", + createdAt: new Date(), + }); + fake.seed(schema.account, { + id: "account-1", + accountId: "account-1", + providerId: "credential", + userId: owner.id, + password: await bcrypt.hash(LOGIN_PASSWORD, 4), + createdAt: new Date(), + updatedAt: new Date(), + }); +}; + +const seedTelegram = ( + id: string, + { + superPassword = true, + organizationId = "org-1", + messageThreadId = null as string | null, + } = {}, +) => { + fake.seed(schema.telegram, { + telegramId: `tg-${id}`, + botToken: `bot-token-${id}`, + chatId: `chat-${id}`, + messageThreadId, + }); + fake.seed(schema.notifications, { + notificationId: id, + name: `Telegram ${id}`, + notificationType: "telegram", + telegramId: `tg-${id}`, + organizationId, + superPassword, + }); +}; + +const seedDiscord = (id: string) => { + fake.seed(schema.discord, { + discordId: `dc-${id}`, + webhookUrl: `https://discord.example.test/${id}`, + decoration: true, + }); + fake.seed(schema.notifications, { + notificationId: id, + name: `Discord ${id}`, + notificationType: "discord", + discordId: `dc-${id}`, + organizationId: "org-1", + superPassword: true, + }); +}; + +const telegramCalls = () => + mocks.senders.sendTelegramNotification.mock.calls as [ + { chatId: string; messageThreadId?: string | null }, + string, + { text: string; url: string }[][] | undefined, + ][]; + +const telegramTexts = (chatId?: string) => + telegramCalls() + .filter(([connection]) => !chatId || connection.chatId === chatId) + .map(([, text]) => text); + +const waitForTelegram = (title: string, chatId?: string) => + vi.waitFor(() => + expect(telegramTexts(chatId).join("\n---\n")).toContain(title), + ); + +const flushAlerts = () => new Promise((resolve) => setTimeout(resolve, 30)); + +beforeEach(async () => { + await flushAlerts(); + fake.reset(); + vi.clearAllMocks(); + for (const sender of Object.values(mocks.senders)) { + sender.mockResolvedValue(undefined); + } + await seedOwner(); +}); + +afterEach(flushAlerts); + +describe("'Super password' notification toggle", () => { + const shapeOf = (zodSchema: unknown) => { + const value = zodSchema as { + shape?: Record; + _def?: { schema?: { shape?: Record } }; + }; + return value.shape ?? value._def?.schema?.shape ?? {}; + }; + + it("is part of every create and update schema", () => { + const types = [ + "Slack", + "Telegram", + "Discord", + "Email", + "Resend", + "Gotify", + "Ntfy", + "Mattermost", + "Custom", + "Lark", + "Teams", + "Pushover", + ]; + const missing = types.flatMap((type) => + [`apiCreate${type}`, `apiUpdate${type}`].filter( + (name) => + !("superPassword" in shapeOf(schema[name as keyof typeof schema])), + ), + ); + expect(missing).toEqual([]); + }); + + it("persists on create and update", async () => { + await browser().notification.createTelegram({ + name: "TgBot", + botToken: "bot-token-new", + chatId: "chat-new", + messageThreadId: "", + appDeploy: false, + appBuildError: false, + databaseBackup: false, + dokployBackup: false, + volumeBackup: false, + dokployRestart: false, + dockerCleanup: false, + serverThreshold: false, + superPassword: true, + }); + const [created] = fake.rows(schema.notifications); + expect(created?.superPassword).toBe(true); + + await browser().notification.updateTelegram({ + notificationId: String(created?.notificationId), + telegramId: String(created?.telegramId), + superPassword: false, + }); + expect(fake.rows(schema.notifications)[0]?.superPassword).toBe(false); + }); + + it("announces turning it on and off to every channel, including the old state", async () => { + seedTelegram("watcher"); + seedTelegram("target", { superPassword: false }); + + await browser().notification.updateTelegram({ + notificationId: "target", + telegramId: "tg-target", + superPassword: true, + }); + await waitForTelegram("now receives super password alerts", "chat-watcher"); + await waitForTelegram("now receives super password alerts", "chat-target"); + + mocks.senders.sendTelegramNotification.mockClear(); + await browser().notification.updateTelegram({ + notificationId: "target", + telegramId: "tg-target", + superPassword: false, + }); + await waitForTelegram( + "no longer receives super password alerts", + "chat-watcher", + ); + await waitForTelegram( + "no longer receives super password alerts", + "chat-target", + ); + + mocks.senders.sendTelegramNotification.mockClear(); + await browser().notification.updateTelegram({ + notificationId: "watcher", + telegramId: "tg-watcher", + botToken: "bot-token-other", + chatId: "chat-attacker", + }); + await waitForTelegram("changed its destination", "chat-watcher"); + await waitForTelegram("changed its destination", "chat-attacker"); + + mocks.senders.sendTelegramNotification.mockClear(); + await browser().notification.remove({ notificationId: "watcher" }); + await waitForTelegram( + "no longer receives super password alerts", + "chat-attacker", + ); + }); +}); + +describe("super password alerts", () => { + it("go only to channels with the toggle on, in organizations the user manages", async () => { + seedTelegram("on"); + seedTelegram("off", { superPassword: false }); + seedTelegram("elsewhere", { organizationId: "org-2" }); + fake.seed(schema.member, { + id: "member-2", + organizationId: "org-2", + userId: owner.id, + role: "member", + createdAt: new Date(), + }); + + await service.sendSuperPasswordAlert({ type: "set" }, { user: owner }); + + expect(telegramCalls().map(([connection]) => connection.chatId)).toEqual([ + "chat-on", + ]); + }); + + it("covers every super password event with no secrets in the payloads", async () => { + seedTelegram("alerts", { messageThreadId: "42" }); + seedDiscord("backup"); + const caller = browser(); + + await caller.superPassword.set({ password: SUPER_PASSWORD, hint: HINT }); + await waitForTelegram("Super password set"); + + await caller.superPassword.unlock({ password: SUPER_PASSWORD }); + await waitForTelegram("Super password access opened"); + + await caller.superPassword.extend(); + await waitForTelegram("Super password access extended"); + + await caller.superPassword.close(); + await waitForTelegram("Access was closed from the panel."); + + await expect( + caller.superPassword.unlock({ password: "wrong-password-1" }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + await waitForTelegram("Wrong super password entered"); + + await caller.superPassword.change({ + currentPassword: SUPER_PASSWORD, + password: "super-password-2", + hint: HINT, + }); + await waitForTelegram("Super password changed"); + + await caller.superPassword.requestReset({ notificationId: "backup" }); + await waitForTelegram("Super password reset link requested"); + const resetUrl = ( + mocks.senders.sendDiscordNotification.mock.calls.at(-1)?.[1] as { + description: string; + } + ).description.match(/\((https:[^)]+)\)/)?.[1]; + expect(resetUrl).toContain( + "https://panel.example.test/dashboard/settings/super-password-reset?token=", + ); + + await caller.superPassword.resetWithToken({ + token: new URL(String(resetUrl)).searchParams.get("token") ?? "", + password: "super-password-3", + }); + await waitForTelegram("reset with a reset link"); + + const sentBefore = telegramCalls().length; + await caller.superPassword.unlock({ password: "super-password-3" }); + const openedAlert = () => + telegramCalls() + .slice(sentBefore) + .find(([, text]) => text.includes("Super password access opened")); + await vi.waitFor(() => expect(openedAlert()).toBeDefined()); + const lockUrl = String(openedAlert()?.[2]?.[0]?.[0]?.url); + expect(lockUrl).toContain( + "https://panel.example.test/super-password/lock?token=", + ); + await caller.superPassword.lockWithToken({ + token: new URL(lockUrl).searchParams.get("token") ?? "", + }); + await waitForTelegram("Access was closed with the"); + + await caller.superPassword.unlock({ password: "super-password-3" }); + await service.lockSuperSessionOverSsh(owner.id); + await waitForTelegram("closed over SSH"); + + for (let attempt = 0; attempt < 5; attempt++) { + await caller.superPassword + .unlock({ password: "wrong-password-1" }) + .catch(() => undefined); + } + await waitForTelegram("Super password locked"); + + await service.resetSuperPasswordOverSsh(owner.id); + await waitForTelegram("removed over SSH"); + + await caller.superPassword.set({ password: SUPER_PASSWORD }); + await caller.superPassword.disable({ password: SUPER_PASSWORD }); + await waitForTelegram("Super password turned off"); + + for (const [connection] of telegramCalls()) { + expect(connection).toMatchObject({ messageThreadId: "42" }); + } + + const messages = [ + ...telegramCalls().map(([, text, buttons]) => ({ text, buttons })), + ...mocks.senders.sendDiscordNotification.mock.calls.map( + ([, embed]) => embed, + ), + ]; + const serialized = JSON.stringify(messages); + const hashes = fake + .rows(schema.superPasswordToken) + .map((row) => String(row.tokenHash)); + for (const secret of [ + HINT, + SUPER_PASSWORD, + "super-password-2", + "super-password-3", + LOGIN_PASSWORD, + "$2b$", + "bot-token-alerts", + ...hashes, + ]) { + expect(serialized, secret).not.toContain(secret); + } + const telegramOnly = JSON.stringify( + telegramCalls().map(([, text, buttons]) => ({ text, buttons })), + ); + expect(telegramOnly).not.toContain("super-password-reset?token="); + }); + + it("puts the one-time lock link on opened and extended alerts as a Telegram button", async () => { + seedTelegram("alerts"); + await service.setSuperPassword({ + userId: owner.id, + password: SUPER_PASSWORD, + }); + const caller = browser(); + + await caller.superPassword.unlock({ password: SUPER_PASSWORD }); + await caller.superPassword.extend(); + await waitForTelegram("Super password access extended"); + + for (const title of [ + "Super password access opened", + "Super password access extended", + ]) { + const [, text, buttons] = + telegramCalls().find(([, message]) => message.includes(title)) ?? []; + expect(text).toContain("Time left"); + expect(text).toContain("203.0.113.7"); + expect(buttons?.[0]?.[0]).toMatchObject({ + text: "This wasn't me – close access", + url: expect.stringContaining( + "https://panel.example.test/super-password/lock?token=", + ), + }); + } + expect( + fake.rows(schema.superPasswordToken).filter((row) => row.type === "lock"), + ).toHaveLength(2); + }); + + it("never blocks unlock or extend on a failing or hanging channel", async () => { + seedTelegram("broken"); + seedDiscord("hanging"); + mocks.senders.sendTelegramNotification.mockRejectedValue( + new Error("telegram is down"), + ); + mocks.senders.sendDiscordNotification.mockImplementation( + () => new Promise(() => undefined), + ); + await service.setSuperPassword({ + userId: owner.id, + password: SUPER_PASSWORD, + }); + const caller = browser(); + + await expect( + caller.superPassword.unlock({ password: SUPER_PASSWORD }), + ).resolves.toMatchObject({ expiresAt: expect.any(Date) }); + await expect(caller.superPassword.extend()).resolves.toMatchObject({ + expiresAt: expect.any(Date), + }); + await expect(service.getSuperSessionState(owner.id)).resolves.toMatchObject( + { active: true }, + ); + }); +}); + +describe("super password reset over a notification channel", () => { + it("sends the link only to the chosen channel and accepts it once", async () => { + seedTelegram("main"); + seedDiscord("backup"); + await service.setSuperPassword({ + userId: owner.id, + password: SUPER_PASSWORD, + }); + const caller = browser(); + + await expect(caller.superPassword.status()).resolves.toMatchObject({ + recoveryChannels: [ + { + notificationId: "main", + name: "Telegram main", + notificationType: "telegram", + }, + { + notificationId: "backup", + name: "Discord backup", + notificationType: "discord", + }, + ], + }); + + await caller.superPassword.requestReset({ notificationId: "main" }); + const [connection, text, buttons] = telegramCalls()[0] ?? []; + expect(connection).toMatchObject({ chatId: "chat-main" }); + expect(text).toContain("Reset your super password"); + const url = String(buttons?.[0]?.[0]?.url); + expect(url).toContain( + "https://panel.example.test/dashboard/settings/super-password-reset?token=", + ); + await vi.waitFor(() => + expect(mocks.senders.sendDiscordNotification).toHaveBeenCalledTimes(1), + ); + expect( + JSON.stringify(mocks.senders.sendDiscordNotification.mock.calls), + ).not.toContain("super-password-reset?token="); + + const token = new URL(url).searchParams.get("token") ?? ""; + await caller.superPassword.resetWithToken({ + token, + password: "super-password-new", + }); + await expect( + caller.superPassword.resetWithToken({ + token, + password: "super-password-other", + }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + }); + + it("allows 3 reset links per hour", async () => { + seedTelegram("main"); + await service.setSuperPassword({ + userId: owner.id, + password: SUPER_PASSWORD, + }); + const start = new Date("2026-01-01T10:00:00.000Z"); + const request = (now: Date) => + service.requestSuperPasswordReset({ + user: owner, + notificationId: "main", + now, + }); + + await request(start); + await request(new Date("2026-01-01T10:10:00.000Z")); + await request(new Date("2026-01-01T10:20:00.000Z")); + await expect( + request(new Date("2026-01-01T10:30:00.000Z")), + ).rejects.toMatchObject({ code: "TOO_MANY_REQUESTS" }); + await expect( + request(new Date("2026-01-01T11:01:00.000Z")), + ).resolves.toBeUndefined(); + }); + + it("reports no recovery channel when no notification has the toggle on", async () => { + seedTelegram("quiet", { superPassword: false }); + await service.setSuperPassword({ + userId: owner.id, + password: SUPER_PASSWORD, + }); + const caller = browser(); + + await expect(caller.superPassword.status()).resolves.toMatchObject({ + recoveryChannels: [], + }); + await expect( + caller.superPassword.requestReset({ notificationId: "quiet" }), + ).rejects.toMatchObject({ + code: "BAD_REQUEST", + message: + "Pick a notification channel that has the 'Super password' toggle on.", + }); + expect(mocks.senders.sendTelegramNotification).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/dokploy/__test__/security/super-password-service.test.ts b/apps/dokploy/__test__/security/super-password-service.test.ts new file mode 100644 index 00000000000..b76a3c0c1eb --- /dev/null +++ b/apps/dokploy/__test__/security/super-password-service.test.ts @@ -0,0 +1,571 @@ +import bcrypt from "bcrypt"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + sendEmailNotification: vi.fn(), + sendResendNotification: vi.fn(), +})); + +vi.mock("@dokploy/server/db", async () => { + const schema = await import("@dokploy/server/db/schema"); + const { createFakeDrizzleDb } = await import("../helpers/fake-drizzle-db"); + const fake = createFakeDrizzleDb({ + superPassword: schema.superPassword, + superSession: schema.superSession, + superPasswordToken: schema.superPasswordToken, + account: schema.account, + member: schema.member, + notifications: schema.notifications, + email: schema.email, + resend: schema.resend, + }); + return { db: fake.db, fake }; +}); + +vi.mock("@dokploy/server/utils/notifications/utils", () => ({ + sendEmailNotification: mocks.sendEmailNotification, + sendResendNotification: mocks.sendResendNotification, +})); + +vi.mock("@dokploy/server/services/admin", () => ({ + getDokployUrl: async () => "https://panel.example.test", +})); + +const schema = await import("@dokploy/server/db/schema"); +const { fake } = (await import("@dokploy/server/db")) as unknown as { + fake: ReturnType< + typeof import("../helpers/fake-drizzle-db")["createFakeDrizzleDb"] + >; +}; +const service = await import("@dokploy/server/services/super-password"); + +const user = { id: "user-1", email: "owner@example.test" }; +const LOGIN_PASSWORD = "login-password-1"; +const SUPER_PASSWORD = "super-password-1"; + +const seedLoginPassword = async () => { + fake.seed(schema.account, { + id: "account-1", + accountId: "account-1", + providerId: "credential", + userId: user.id, + password: await bcrypt.hash(LOGIN_PASSWORD, 4), + createdAt: new Date(), + updatedAt: new Date(), + }); +}; + +const seedEmailChannel = () => { + fake.seed(schema.member, { + id: "member-1", + organizationId: "org-1", + userId: user.id, + role: "owner", + createdAt: new Date(), + }); + fake.seed(schema.email, { + emailId: "email-1", + smtpServer: "smtp.example.test", + smtpPort: 587, + username: "mailer", + password: "smtp-secret", + fromAddress: "panel@example.test", + toAddresses: ["ops@example.test"], + }); + fake.seed(schema.notifications, { + notificationId: "notification-1", + name: "Email", + notificationType: "email", + emailId: "email-1", + organizationId: "org-1", + superPassword: true, + }); +}; + +const setSuperPassword = (hint?: string | null) => + service.setSuperPassword({ userId: user.id, password: SUPER_PASSWORD, hint }); + +const verify = (password: string, now?: Date) => + service.verifySuperPassword({ + user, + password, + now, + }); + +describe("super password storage", () => { + beforeEach(() => { + fake.reset(); + vi.clearAllMocks(); + }); + + it("stores only a bcrypt hash and verifies it", async () => { + await seedLoginPassword(); + await setSuperPassword("first pet"); + + const [record] = fake.rows(schema.superPassword); + expect(record?.passwordHash).not.toContain(SUPER_PASSWORD); + expect( + await bcrypt.compare(SUPER_PASSWORD, String(record?.passwordHash)), + ).toBe(true); + expect(record?.hint).toBe("first pet"); + await expect( + service.matchesSuperPassword(user.id, SUPER_PASSWORD), + ).resolves.toBe(true); + await expect(verify(SUPER_PASSWORD)).resolves.toBeUndefined(); + }); + + it("rejects a super password equal to the login password", async () => { + await seedLoginPassword(); + + await expect( + service.setSuperPassword({ userId: user.id, password: LOGIN_PASSWORD }), + ).rejects.toMatchObject({ + code: "BAD_REQUEST", + message: "The super password must differ from your login password", + }); + expect(fake.rows(schema.superPassword)).toHaveLength(0); + }); + + it("rejects short passwords and a second set without the current password", async () => { + await expect( + service.setSuperPassword({ userId: user.id, password: "short" }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + + await setSuperPassword(); + await expect(setSuperPassword()).rejects.toMatchObject({ + code: "CONFLICT", + }); + }); + + it("validates the hint", async () => { + expect( + service.normalizeSuperPasswordHint(" ", SUPER_PASSWORD), + ).toBeNull(); + expect(service.normalizeSuperPasswordHint(" pet ", SUPER_PASSWORD)).toBe( + "pet", + ); + expect(() => + service.normalizeSuperPasswordHint("x".repeat(201), SUPER_PASSWORD), + ).toThrow("at most 200 characters"); + expect(() => + service.normalizeSuperPasswordHint( + `it is ${SUPER_PASSWORD.toUpperCase()}!`, + SUPER_PASSWORD, + ), + ).toThrow("must not contain the super password"); + await expect( + setSuperPassword(`hint: ${SUPER_PASSWORD}`), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + }); + + it("changes the password only through the change flow and keeps it apart from the login password", async () => { + await seedLoginPassword(); + await setSuperPassword(); + + await expect( + service.changeSuperPassword({ + userId: user.id, + password: LOGIN_PASSWORD, + }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + + await service.changeSuperPassword({ + userId: user.id, + password: "another-super-2", + hint: "new", + }); + await expect(verify("another-super-2")).resolves.toBeUndefined(); + await expect( + service.matchesSuperPassword(user.id, SUPER_PASSWORD), + ).resolves.toBe(false); + }); +}); + +describe("super password brute-force lock", () => { + beforeEach(() => { + fake.reset(); + vi.clearAllMocks(); + }); + + it("locks for 15 minutes after 5 wrong attempts and alerts the channels", async () => { + seedEmailChannel(); + await setSuperPassword(); + const now = new Date("2026-01-01T10:00:00.000Z"); + + for (let attempt = 1; attempt < 5; attempt++) { + await expect(verify("wrong-password", now)).rejects.toMatchObject({ + code: "BAD_REQUEST", + message: expect.stringContaining(`${5 - attempt} attempt`), + }); + } + await expect(verify("wrong-password", now)).rejects.toMatchObject({ + code: "TOO_MANY_REQUESTS", + }); + + const [record] = fake.rows(schema.superPassword); + expect(record?.lockedUntil).toEqual(new Date("2026-01-01T10:15:00.000Z")); + expect(record?.failedAttempts).toBe(0); + await vi.waitFor(() => + expect(mocks.sendEmailNotification).toHaveBeenCalledWith( + expect.objectContaining({ toAddresses: ["ops@example.test"] }), + "Super password locked", + expect.stringContaining("2026-01-01 10:15 UTC"), + ), + ); + await vi.waitFor(() => + expect( + mocks.sendEmailNotification.mock.calls.filter( + ([, subject]) => subject === "Wrong super password entered", + ), + ).toHaveLength(4), + ); + + await expect( + verify(SUPER_PASSWORD, new Date("2026-01-01T10:14:00.000Z")), + ).rejects.toMatchObject({ code: "TOO_MANY_REQUESTS" }); + + await expect( + verify(SUPER_PASSWORD, new Date("2026-01-01T10:16:00.000Z")), + ).resolves.toBeUndefined(); + expect(fake.rows(schema.superPassword)[0]).toMatchObject({ + failedAttempts: 0, + lockedUntil: null, + }); + }); + + it("still locks when no channel has the super password toggle on", async () => { + await setSuperPassword(); + for (let attempt = 0; attempt < 4; attempt++) { + await expect(verify("wrong-password")).rejects.toMatchObject({ + code: "BAD_REQUEST", + }); + } + await expect(verify("wrong-password")).rejects.toMatchObject({ + code: "TOO_MANY_REQUESTS", + }); + expect(mocks.sendEmailNotification).not.toHaveBeenCalled(); + }); +}); + +describe("super session lifetime", () => { + beforeEach(() => { + fake.reset(); + vi.clearAllMocks(); + }); + + it("opens for 24 hours and extends only while open", async () => { + await setSuperPassword(); + const openedAt = new Date("2026-01-01T00:00:00.000Z"); + + await expect( + service.extendSuperSession(user.id, openedAt), + ).rejects.toMatchObject({ code: "PRECONDITION_FAILED" }); + + const { expiresAt } = await service.openSuperSession({ + userId: user.id, + sessionId: "session-1", + now: openedAt, + }); + expect(expiresAt).toEqual(new Date("2026-01-02T00:00:00.000Z")); + expect(service.SUPER_SESSION_DURATION_MS).toBe(24 * 60 * 60 * 1000); + + const later = new Date("2026-01-01T20:00:00.000Z"); + await expect(service.extendSuperSession(user.id, later)).resolves.toEqual({ + expiresAt: new Date("2026-01-02T20:00:00.000Z"), + }); + expect(fake.rows(schema.superSession)[0]).toMatchObject({ + lastExtendedAt: later, + openedBySessionId: "session-1", + }); + + await expect( + service.extendSuperSession(user.id, new Date("2026-01-03T00:00:00.000Z")), + ).rejects.toMatchObject({ code: "PRECONDITION_FAILED" }); + }); + + it("reports the state without the hash", async () => { + await setSuperPassword("pet"); + await service.openSuperSession({ userId: user.id, sessionId: "session-1" }); + + const state = await service.getSuperSessionState(user.id); + expect(state).toMatchObject({ isSet: true, hint: "pet", active: true }); + expect(JSON.stringify(state)).not.toContain("$2"); + + await service.closeSuperSession(user.id); + await expect(service.getSuperSessionState(user.id)).resolves.toMatchObject({ + active: false, + expiresAt: null, + }); + }); + + it("turning the super password off removes the session and tokens", async () => { + await setSuperPassword(); + await service.openSuperSession({ userId: user.id, sessionId: "session-1" }); + await service.createSuperPasswordToken({ userId: user.id, type: "lock" }); + + await service.removeSuperPassword(user.id); + + expect(fake.rows(schema.superPassword)).toHaveLength(0); + expect(fake.rows(schema.superSession)).toHaveLength(0); + expect(fake.rows(schema.superPasswordToken)).toHaveLength(0); + }); +}); + +describe("super password tokens", () => { + beforeEach(() => { + fake.reset(); + vi.clearAllMocks(); + }); + + it("stores only the token hash and accepts each token once", async () => { + const { token } = await service.createSuperPasswordToken({ + userId: user.id, + type: "reset", + }); + expect(fake.rows(schema.superPasswordToken)[0]?.tokenHash).not.toBe(token); + + await expect( + service.consumeSuperPasswordToken({ token, type: "lock" }), + ).resolves.toBeNull(); + await expect( + service.consumeSuperPasswordToken({ + token, + type: "reset", + userId: "user-2", + }), + ).resolves.toBeNull(); + await expect( + service.consumeSuperPasswordToken({ + token, + type: "reset", + userId: user.id, + }), + ).resolves.toBe(user.id); + await expect( + service.consumeSuperPasswordToken({ + token, + type: "reset", + userId: user.id, + }), + ).resolves.toBeNull(); + }); + + it("rejects expired tokens and cleans them up", async () => { + const createdAt = new Date("2026-01-01T00:00:00.000Z"); + const { token } = await service.createSuperPasswordToken({ + userId: user.id, + type: "reset", + now: createdAt, + }); + + await expect( + service.consumeSuperPasswordToken({ + token, + type: "reset", + now: new Date("2026-01-01T00:31:00.000Z"), + }), + ).resolves.toBeNull(); + + await service.createSuperPasswordToken({ + userId: "user-2", + type: "lock", + now: new Date("2026-01-01T01:00:00.000Z"), + }); + expect(fake.rows(schema.superPasswordToken)).toHaveLength(1); + expect(fake.rows(schema.superPasswordToken)[0]?.userId).toBe("user-2"); + }); + + it("lock links only close access, once", async () => { + await setSuperPassword(); + await service.openSuperSession({ userId: user.id, sessionId: "session-1" }); + const { token } = await service.createSuperPasswordToken({ + userId: user.id, + type: "lock", + }); + + await expect(service.closeSuperSessionWithToken(token)).resolves.toBe(true); + await expect(service.getSuperSessionState(user.id)).resolves.toMatchObject({ + active: false, + }); + await expect(service.closeSuperSessionWithToken(token)).resolves.toBe( + false, + ); + await expect(service.getSuperSessionState(user.id)).resolves.toMatchObject({ + active: false, + }); + }); + + it("resets with a single-use link for the same user and closes access", async () => { + await seedLoginPassword(); + await setSuperPassword(); + await service.openSuperSession({ userId: user.id, sessionId: "session-1" }); + const { token } = await service.createSuperPasswordToken({ + userId: user.id, + type: "reset", + }); + + await expect( + service.resetSuperPasswordWithToken({ + userId: "user-2", + token, + password: "brand-new-super", + }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + + await service.resetSuperPasswordWithToken({ + userId: user.id, + token, + password: "brand-new-super", + hint: "new hint", + }); + + await expect(verify("brand-new-super")).resolves.toBeUndefined(); + await expect(service.getSuperSessionState(user.id)).resolves.toMatchObject({ + active: false, + hint: "new hint", + }); + await expect( + service.resetSuperPasswordWithToken({ + userId: user.id, + token, + password: "third-super-pass", + }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + }); + + it("sends reset links and unlock alerts through an email channel with the toggle on", async () => { + await setSuperPassword(); + seedEmailChannel(); + + await service.requestSuperPasswordReset({ + user, + notificationId: "notification-1", + }); + const [resetConnection, resetSubject, resetHtml] = + mocks.sendEmailNotification.mock.calls[0] ?? []; + expect(resetConnection).toMatchObject({ + toAddresses: ["ops@example.test"], + }); + expect(resetSubject).toBe("Reset your super password"); + expect(resetHtml).toContain( + "https://panel.example.test/dashboard/settings/super-password-reset?token=", + ); + + mocks.sendEmailNotification.mockClear(); + await service.sendSuperPasswordAlert( + { type: "opened", expiresAt: new Date(Date.now() + 86_400_000) }, + { user, ipAddress: "203.0.113.7", userAgent: "Browser/1.0" }, + ); + const [, subject, html] = mocks.sendEmailNotification.mock.calls[0] ?? []; + expect(subject).toBe("Super password access opened"); + expect(html).toContain("203.0.113.7"); + expect(html).toContain("Browser/1.0"); + expect(html).toContain( + "https://panel.example.test/super-password/lock?token=", + ); + }); +}); + +describe("super session access rules", () => { + const notSet = { isSet: false, active: false }; + const closed = { isSet: true, active: false }; + const open = { isSet: true, active: true }; + + it("keeps today's behaviour while no super password is set", () => { + for (const kind of ["read", "write", "dangerous"] as const) { + for (const viaApiKey of [true, false]) { + expect( + service.evaluateSuperSessionAccess({ + state: notSet, + kind, + viaApiKey, + }), + ).toBeNull(); + } + } + }); + + it("locks API key writes while closed and never lets API keys do dangerous actions", () => { + expect( + service.evaluateSuperSessionAccess({ + state: closed, + kind: "write", + viaApiKey: true, + }), + ).toBe("api-key-write-locked"); + expect( + service.evaluateSuperSessionAccess({ + state: open, + kind: "write", + viaApiKey: true, + }), + ).toBeNull(); + expect( + service.evaluateSuperSessionAccess({ + state: closed, + kind: "read", + viaApiKey: true, + }), + ).toBeNull(); + expect( + service.evaluateSuperSessionAccess({ + state: open, + kind: "dangerous", + viaApiKey: true, + }), + ).toBe("browser-session-required"); + expect( + service.evaluateSuperSessionAccess({ + state: closed, + kind: "dangerous", + viaApiKey: false, + }), + ).toBe("super-session-required"); + expect( + service.evaluateSuperSessionAccess({ + state: open, + kind: "dangerous", + viaApiKey: false, + }), + ).toBeNull(); + expect( + service.evaluateSuperSessionAccess({ + state: closed, + kind: "write", + viaApiKey: false, + }), + ).toBeNull(); + }); + + it("keeps TLS files and system containers closed without an open super session", async () => { + fake.reset(); + await expect( + service.checkProtectedResourceAccess({ + userId: user.id, + viaApiKey: false, + }), + ).resolves.toBe("super-session-required"); + + await setSuperPassword(); + await expect( + service.checkProtectedResourceAccess({ + userId: user.id, + viaApiKey: false, + }), + ).resolves.toBe("super-session-required"); + + await service.openSuperSession({ userId: user.id, sessionId: "session-1" }); + await expect( + service.checkProtectedResourceAccess({ + userId: user.id, + viaApiKey: false, + }), + ).resolves.toBeNull(); + await expect( + service.checkProtectedResourceAccess({ + userId: user.id, + viaApiKey: true, + }), + ).resolves.toBe("browser-session-required"); + }); +}); diff --git a/apps/dokploy/__test__/security/super-password-telegram.test.ts b/apps/dokploy/__test__/security/super-password-telegram.test.ts new file mode 100644 index 00000000000..a5b4d4a4b75 --- /dev/null +++ b/apps/dokploy/__test__/security/super-password-telegram.test.ts @@ -0,0 +1,128 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + baseUrl: "https://panel.example.test", + fetch: vi.fn(), +})); + +vi.mock("@dokploy/server/db", async () => { + const schema = await import("@dokploy/server/db/schema"); + const { createFakeDrizzleDb } = await import("../helpers/fake-drizzle-db"); + const fake = createFakeDrizzleDb({ + superPassword: schema.superPassword, + superSession: schema.superSession, + superPasswordToken: schema.superPasswordToken, + member: schema.member, + user: schema.user, + notifications: schema.notifications, + telegram: schema.telegram, + }); + return { db: fake.db, fake }; +}); + +vi.mock("@dokploy/server/services/admin", () => ({ + getDokployUrl: async () => mocks.baseUrl, +})); + +const schema = await import("@dokploy/server/db/schema"); +const { fake } = (await import("@dokploy/server/db")) as unknown as { + fake: ReturnType< + typeof import("../helpers/fake-drizzle-db")["createFakeDrizzleDb"] + >; +}; +const service = await import("@dokploy/server/services/super-password"); + +const user = { id: "user-1", email: "owner@example.test" }; + +const telegramBodies = () => + mocks.fetch.mock.calls.map(([url, init]) => ({ + url: String(url), + body: JSON.parse(String((init as RequestInit).body)), + })); + +describe("super password alerts over the Telegram Bot API", () => { + beforeEach(() => { + fake.reset(); + mocks.fetch.mockReset(); + mocks.fetch.mockResolvedValue(new Response("{}", { status: 200 })); + vi.stubGlobal("fetch", mocks.fetch); + mocks.baseUrl = "https://panel.example.test"; + fake.seed(schema.member, { + id: "member-1", + organizationId: "org-1", + userId: user.id, + role: "owner", + createdAt: new Date(), + }); + fake.seed(schema.telegram, { + telegramId: "tg-1", + botToken: "123:bot-token", + chatId: "-100200300", + messageThreadId: "42", + }); + fake.seed(schema.notifications, { + notificationId: "notification-1", + name: "TgBot - alerts", + notificationType: "telegram", + telegramId: "tg-1", + organizationId: "org-1", + superPassword: true, + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("sends an HTML message to the chat and thread with an inline lock button", async () => { + await service.sendSuperPasswordAlert( + { type: "opened", expiresAt: new Date(Date.now() + 86_400_000) }, + { user, ipAddress: "203.0.113.7", userAgent: "Browser <1.0>" }, + ); + + const [request] = telegramBodies(); + expect(request?.url).toBe( + "https://api.telegram.org/bot123:bot-token/sendMessage", + ); + expect(request?.body).toMatchObject({ + chat_id: "-100200300", + message_thread_id: "42", + parse_mode: "HTML", + disable_web_page_preview: true, + reply_markup: { + inline_keyboard: [ + [ + { + text: "This wasn't me – close access", + url: expect.stringMatching( + /^https:\/\/panel\.example\.test\/super-password\/lock\?token=[\w-]+$/, + ), + }, + ], + ], + }, + }); + expect(request?.body.text).toContain( + "🔐 Super password access opened", + ); + expect(request?.body.text).toContain( + "User agent: Browser <1.0>", + ); + expect(request?.body.text).toContain("Time left: 24 h 0 min"); + }); + + it("falls back to a plain link when the panel URL cannot be a Telegram button", async () => { + mocks.baseUrl = "http://203.0.113.10:3000"; + + await service.sendSuperPasswordAlert( + { type: "extended", expiresAt: new Date(Date.now() + 86_400_000) }, + { user }, + ); + + const [request] = telegramBodies(); + expect(request?.body.reply_markup.inline_keyboard).toBeUndefined(); + expect(request?.body.text).toMatch( + /This wasn't me – close access<\/a>/, + ); + }); +}); diff --git a/apps/dokploy/__test__/security/super-session-auth-hook.test.ts b/apps/dokploy/__test__/security/super-session-auth-hook.test.ts new file mode 100644 index 00000000000..66483edd555 --- /dev/null +++ b/apps/dokploy/__test__/security/super-session-auth-hook.test.ts @@ -0,0 +1,152 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + getSessionFromCtx: vi.fn(), +})); + +vi.mock("better-auth/api", async (importOriginal) => ({ + ...(await importOriginal()), + getSessionFromCtx: mocks.getSessionFromCtx, +})); + +vi.mock("@dokploy/server/db", async () => { + const schema = await import("@dokploy/server/db/schema"); + const { createFakeDrizzleDb } = await import("../helpers/fake-drizzle-db"); + const fake = createFakeDrizzleDb({ + superPassword: schema.superPassword, + superSession: schema.superSession, + account: schema.account, + }); + return { db: fake.db, fake }; +}); + +const { fake } = (await import("@dokploy/server/db")) as unknown as { + fake: ReturnType< + typeof import("../helpers/fake-drizzle-db")["createFakeDrizzleDb"] + >; +}; +const service = await import("@dokploy/server/services/super-password"); +const { + enforceSuperSessionForAuthEndpoint, + isSuperSessionAuthPath, + SUPER_SESSION_AUTH_ERROR_CODE, +} = await import("@dokploy/server/lib/super-session-auth"); + +type AuthCtx = Parameters[0]; +const ctxFor = (path: string) => ({ path }) as unknown as AuthCtx; + +const GATED = [ + "/api-key/create", + "/api-key/delete", + "/change-email", + "/change-password", + "/delete-user", + "/organization/invite-member", + "/organization/remove-member", + "/organization/update-member-role", + "/organization/create-role", + "/two-factor/enable", + "/two-factor/disable", + "/two-factor/get-totp-uri", + "/passkey/generate-register-options", + "/passkey/delete-passkey", + "/sso/update-provider", + "/sso/delete-provider", +]; + +const NEVER_GATED = [ + "/sign-in/email", + "/sign-in/social", + "/sign-in/sso", + "/sign-out", + "/get-session", + "/refresh-token", + "/update-session", + "/two-factor/verify-totp", + "/two-factor/verify-otp", + "/two-factor/verify-backup-code", + "/two-factor/send-otp", + "/passkey/generate-authenticate-options", + "/passkey/verify-authentication", + "/request-password-reset", + "/reset-password", + "/revoke-session", + "/revoke-other-sessions", + "/organization/set-active", +]; + +describe("better-auth super session gate", () => { + beforeEach(async () => { + fake.reset(); + vi.clearAllMocks(); + mocks.getSessionFromCtx.mockResolvedValue({ + session: { id: "session-1" }, + user: { id: "user-1" }, + }); + await service.setSuperPassword({ + userId: "user-1", + password: "super-password-1", + }); + }); + + it("covers access-changing endpoints but not sign-in, sign-out or 2FA verification", () => { + expect(GATED.filter((path) => !isSuperSessionAuthPath(path))).toEqual([]); + expect(NEVER_GATED.filter((path) => isSuperSessionAuthPath(path))).toEqual( + [], + ); + expect(isSuperSessionAuthPath("/api/auth/two-factor/disable")).toBe(true); + }); + + it("blocks gated endpoints while the super session is closed", async () => { + for (const path of GATED) { + await expect( + enforceSuperSessionForAuthEndpoint(ctxFor(path)), + path, + ).rejects.toMatchObject({ + statusCode: 403, + body: { + code: SUPER_SESSION_AUTH_ERROR_CODE, + message: service.SUPER_SESSION_MESSAGES["super-session-required"], + }, + }); + } + }); + + it("allows gated endpoints while the super session is open", async () => { + await service.openSuperSession({ + userId: "user-1", + sessionId: "session-1", + }); + + for (const path of GATED) { + await expect( + enforceSuperSessionForAuthEndpoint(ctxFor(path)), + path, + ).resolves.toBeUndefined(); + } + }); + + it("never looks up the session for sign-in and verification endpoints", async () => { + for (const path of NEVER_GATED) { + await expect( + enforceSuperSessionForAuthEndpoint(ctxFor(path)), + ).resolves.toBeUndefined(); + } + expect(mocks.getSessionFromCtx).not.toHaveBeenCalled(); + }); + + it("leaves users without a super password and anonymous requests unchanged", async () => { + mocks.getSessionFromCtx.mockResolvedValue({ + session: { id: "session-2" }, + user: { id: "user-2" }, + }); + await expect( + enforceSuperSessionForAuthEndpoint(ctxFor("/two-factor/disable")), + ).resolves.toBeUndefined(); + + mocks.getSessionFromCtx.mockResolvedValue(null); + await expect( + enforceSuperSessionForAuthEndpoint(ctxFor("/two-factor/disable")), + ).resolves.toBeUndefined(); + }); +}); diff --git a/apps/dokploy/__test__/security/super-session-env-upsert.test.ts b/apps/dokploy/__test__/security/super-session-env-upsert.test.ts new file mode 100644 index 00000000000..ddb84b5fcc4 --- /dev/null +++ b/apps/dokploy/__test__/security/super-session-env-upsert.test.ts @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + audit: vi.fn(), + checkServicePermissionAndAccess: vi.fn(), + findApplicationById: vi.fn(), + upsertApplicationEnvironment: vi.fn(), + validateRequest: vi.fn(), +})); + +vi.mock("@dokploy/server", () => ({ + IS_CLOUD: false, + findApplicationById: mocks.findApplicationById, + upsertApplicationEnvironment: mocks.upsertApplicationEnvironment, + validateRequest: mocks.validateRequest, +})); + +vi.mock("@dokploy/server/db", async () => { + const schema = await import("@dokploy/server/db/schema"); + const { createFakeDrizzleDb } = await import("../helpers/fake-drizzle-db"); + const fake = createFakeDrizzleDb({ + superPassword: schema.superPassword, + superSession: schema.superSession, + account: schema.account, + }); + return { db: fake.db, fake }; +}); + +vi.mock("@dokploy/server/services/permission", () => ({ + checkServicePermissionAndAccess: mocks.checkServicePermissionAndAccess, +})); + +vi.mock("@/server/api/utils/audit", () => ({ audit: mocks.audit })); +vi.mock("@/server/queues/queueSetup", () => ({ myQueue: { add: vi.fn() } })); +vi.mock("@/server/utils/deploy", () => ({ deploy: vi.fn() })); + +const { fake } = (await import("@dokploy/server/db")) as unknown as { + fake: ReturnType< + typeof import("../helpers/fake-drizzle-db")["createFakeDrizzleDb"] + >; +}; +const service = await import("@dokploy/server/services/super-password"); +const { handleApplicationEnvUpsert } = await import( + "@/pages/api/application.env.upsert" +); + +const createResponse = () => { + const response = { + statusCode: 0, + body: undefined as unknown, + setHeader: vi.fn(), + status(code: number) { + response.statusCode = code; + return response; + }, + json(body: unknown) { + response.body = body; + return response; + }, + }; + return response; +}; + +const callUpsert = async () => { + const response = createResponse(); + await handleApplicationEnvUpsert( + { + method: "POST", + body: { applicationId: "app-1", variables: { API_URL: "https://x" } }, + } as never, + response as never, + ); + return response; +}; + +describe("application env upsert endpoint and the super session", () => { + beforeEach(() => { + fake.reset(); + vi.clearAllMocks(); + mocks.validateRequest.mockResolvedValue({ + user: { id: "user-1", role: "owner", email: "owner@example.test" }, + session: { activeOrganizationId: "org-1", authMethod: "api-key" }, + }); + mocks.checkServicePermissionAndAccess.mockResolvedValue(undefined); + mocks.upsertApplicationEnvironment.mockResolvedValue({ + changed: false, + dryRun: false, + }); + }); + + it("keeps API key writes working while no super password is set", async () => { + const response = await callUpsert(); + + expect(response.statusCode).toBe(200); + expect(mocks.upsertApplicationEnvironment).toHaveBeenCalled(); + }); + + it("rejects API key writes while the super session is closed", async () => { + await service.setSuperPassword({ + userId: "user-1", + password: "super-password-1", + }); + + const response = await callUpsert(); + + expect(response.statusCode).toBe(403); + expect(response.body).toEqual({ + message: service.SUPER_SESSION_MESSAGES["api-key-write-locked"], + }); + expect(mocks.upsertApplicationEnvironment).not.toHaveBeenCalled(); + }); + + it("accepts API key writes while the super session is open", async () => { + await service.setSuperPassword({ + userId: "user-1", + password: "super-password-1", + }); + await service.openSuperSession({ userId: "user-1", sessionId: "session-1" }); + + const response = await callUpsert(); + + expect(response.statusCode).toBe(200); + expect(mocks.upsertApplicationEnvironment).toHaveBeenCalled(); + }); +}); diff --git a/apps/dokploy/__test__/security/super-session-guard.test.ts b/apps/dokploy/__test__/security/super-session-guard.test.ts new file mode 100644 index 00000000000..e309b20cc98 --- /dev/null +++ b/apps/dokploy/__test__/security/super-session-guard.test.ts @@ -0,0 +1,441 @@ +import { TRPCError } from "@trpc/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod"; + +const mocks = vi.hoisted(() => ({ + audit: vi.fn(), + sendEmailNotification: vi.fn(), + sendResendNotification: vi.fn(), +})); + +vi.mock("@dokploy/server/db", async () => { + const schema = await import("@dokploy/server/db/schema"); + const { createFakeDrizzleDb } = await import("../helpers/fake-drizzle-db"); + const fake = createFakeDrizzleDb({ + superPassword: schema.superPassword, + superSession: schema.superSession, + superPasswordToken: schema.superPasswordToken, + account: schema.account, + member: schema.member, + notifications: schema.notifications, + email: schema.email, + resend: schema.resend, + schedules: schema.schedules, + backups: schema.backups, + }); + return { db: fake.db, fake }; +}); + +vi.mock("@dokploy/server/utils/notifications/utils", () => ({ + sendEmailNotification: mocks.sendEmailNotification, + sendResendNotification: mocks.sendResendNotification, +})); + +vi.mock("@dokploy/server/services/admin", () => ({ + getDokployUrl: async () => "https://panel.example.test", + getTrustedOrigins: async () => [], + getUserByToken: vi.fn(), +})); + +vi.mock("@/server/api/utils/audit", () => ({ + audit: mocks.audit, +})); + +const schema = await import("@dokploy/server/db/schema"); +const { fake } = (await import("@dokploy/server/db")) as unknown as { + fake: ReturnType< + typeof import("../helpers/fake-drizzle-db")["createFakeDrizzleDb"] + >; +}; +const service = await import("@dokploy/server/services/super-password"); +const { createTRPCRouter, protectedProcedure, publicProcedure } = await import( + "@/server/api/trpc" +); +const { superPasswordRouter } = await import( + "@/server/api/routers/super-password" +); +const { userRouter } = await import("@/server/api/routers/user"); + +const handled = vi.fn((name: string) => name); + +const testRouter = createTRPCRouter({ + application: createTRPCRouter({ + one: protectedProcedure.query(() => handled("application.one")), + update: protectedProcedure.mutation(() => handled("application.update")), + revealEnvironment: protectedProcedure.mutation(() => + handled("application.revealEnvironment"), + ), + }), + user: createTRPCRouter({ + update: protectedProcedure + .input( + z.object({ + email: z.string().optional(), + firstName: z.string().optional(), + }), + ) + .mutation(() => handled("user.update")), + }), + schedule: createTRPCRouter({ + create: protectedProcedure + .input(z.object({ scheduleType: z.string() })) + .mutation(() => handled("schedule.create")), + runManually: protectedProcedure + .input(z.object({ scheduleId: z.string() })) + .mutation(() => handled("schedule.runManually")), + }), + settings: createTRPCRouter({ + health: publicProcedure.query(() => handled("settings.health")), + }), + superPassword: superPasswordRouter, +}); + +const owner = { + id: "user-1", + email: "owner@example.test", + role: "owner" as const, + ownerId: "user-1", +}; + +const browserCaller = () => + testRouter.createCaller({ + db: {}, + req: { + headers: { "user-agent": "Browser/1.0", "x-real-ip": "203.0.113.7" }, + }, + res: {}, + user: owner, + session: { + id: "session-1", + userId: owner.id, + activeOrganizationId: "org-1", + authMethod: "session", + }, + } as never); + +const apiKeyCaller = () => + testRouter.createCaller({ + db: {}, + req: { headers: {} }, + res: {}, + user: owner, + session: { + userId: owner.id, + activeOrganizationId: "org-1", + authMethod: "api-key", + apiKeyId: "key-1", + }, + } as never); + +const setSuperPassword = () => + service.setSuperPassword({ userId: owner.id, password: "super-password-1" }); + +const openSuperSession = () => + service.openSuperSession({ userId: owner.id, sessionId: "session-1" }); + +const expectSuperSessionError = async ( + promise: Promise, + reason: + | "api-key-write-locked" + | "super-session-required" + | "browser-session-required", +) => { + const error = await promise.then( + () => null, + (caught: unknown) => caught, + ); + expect(error).toBeInstanceOf(TRPCError); + expect(error).toMatchObject({ + code: "FORBIDDEN", + message: service.SUPER_SESSION_MESSAGES[reason], + }); + expect(service.getSuperSessionDenial(error)).toBe(reason); +}; + +beforeEach(() => { + fake.reset(); + vi.clearAllMocks(); +}); + +describe("super session guard without a super password", () => { + it("keeps today's behaviour for API keys and browser sessions", async () => { + await expect(apiKeyCaller().application.update()).resolves.toBe( + "application.update", + ); + await expect(apiKeyCaller().application.revealEnvironment()).resolves.toBe( + "application.revealEnvironment", + ); + await expect(browserCaller().application.revealEnvironment()).resolves.toBe( + "application.revealEnvironment", + ); + await expect( + browserCaller().user.update({ email: "new@example.test" }), + ).resolves.toBe("user.update"); + }); +}); + +describe("super session guard with a super password", () => { + beforeEach(async () => { + await setSuperPassword(); + }); + + it("makes API keys read-only while the super session is closed", async () => { + await expect(apiKeyCaller().application.one()).resolves.toBe( + "application.one", + ); + await expectSuperSessionError( + apiKeyCaller().application.update(), + "api-key-write-locked", + ); + expect(handled).not.toHaveBeenCalledWith("application.update"); + }); + + it("allows API key writes while the super session is open", async () => { + await openSuperSession(); + await expect(apiKeyCaller().application.update()).resolves.toBe( + "application.update", + ); + }); + + it("never lets API keys run dangerous procedures, even while open", async () => { + await expectSuperSessionError( + apiKeyCaller().application.revealEnvironment(), + "browser-session-required", + ); + await openSuperSession(); + await expectSuperSessionError( + apiKeyCaller().application.revealEnvironment(), + "browser-session-required", + ); + expect(handled).not.toHaveBeenCalledWith("application.revealEnvironment"); + }); + + it("needs an open super session for dangerous procedures from the browser", async () => { + await expect(browserCaller().application.update()).resolves.toBe( + "application.update", + ); + await expectSuperSessionError( + browserCaller().application.revealEnvironment(), + "super-session-required", + ); + + await openSuperSession(); + await expect(browserCaller().application.revealEnvironment()).resolves.toBe( + "application.revealEnvironment", + ); + }); + + it("treats only credential changes in user.update as dangerous", async () => { + await expect( + browserCaller().user.update({ firstName: "New", email: owner.email }), + ).resolves.toBe("user.update"); + await expectSuperSessionError( + browserCaller().user.update({ email: "attacker@example.test" }), + "super-session-required", + ); + }); + + it("treats host-level schedules as dangerous", async () => { + await expect( + browserCaller().schedule.create({ scheduleType: "server" }), + ).resolves.toBe("schedule.create"); + await expectSuperSessionError( + browserCaller().schedule.create({ scheduleType: "dokploy-server" }), + "super-session-required", + ); + + fake.seed(schema.schedules, { + scheduleId: "schedule-1", + name: "host", + cronExpression: "* * * * *", + command: "true", + scheduleType: "dokploy-server", + }); + await expectSuperSessionError( + browserCaller().schedule.runManually({ scheduleId: "schedule-1" }), + "super-session-required", + ); + await openSuperSession(); + await expect( + browserCaller().schedule.runManually({ scheduleId: "schedule-1" }), + ).resolves.toBe("schedule.runManually"); + }); + + it("adds the denial reason to the error shape for the UI", () => { + const error = new TRPCError({ + code: "FORBIDDEN", + message: service.SUPER_SESSION_MESSAGES["super-session-required"], + cause: new service.SuperSessionError("super-session-required"), + }); + const formatter = ( + testRouter as unknown as { + _def: { + _config: { + errorFormatter: (opts: unknown) => { + data: { superSession: unknown }; + }; + }; + }; + } + )._def._config.errorFormatter; + + expect( + formatter({ error, shape: { data: {} }, type: "mutation", path: "x" }) + .data.superSession, + ).toBe("super-session-required"); + expect( + formatter({ + error: new TRPCError({ code: "FORBIDDEN" }), + shape: { data: {} }, + type: "mutation", + path: "x", + }).data.superSession, + ).toBeNull(); + }); +}); + +describe("superPassword router", () => { + it("reports status to API keys without the hash", async () => { + await setSuperPassword(); + const status = await apiKeyCaller().superPassword.status(); + + expect(status).toMatchObject({ + isSet: true, + active: false, + expiresAt: null, + viaApiKey: true, + }); + expect(JSON.stringify(status)).not.toContain("$2"); + }); + + it("only lets browser sessions set, unlock and extend", async () => { + await expect( + apiKeyCaller().superPassword.set({ password: "super-password-1" }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }); + + await browserCaller().superPassword.set({ password: "super-password-1" }); + + await expect(browserCaller().superPassword.extend()).rejects.toMatchObject({ + code: "PRECONDITION_FAILED", + }); + await expect( + apiKeyCaller().superPassword.unlock({ password: "super-password-1" }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }); + + const unlocked = await browserCaller().superPassword.unlock({ + password: "super-password-1", + }); + expect(unlocked.expiresAt.getTime()).toBeGreaterThan(Date.now()); + expect(fake.rows(schema.superSession)[0]).toMatchObject({ + openedBySessionId: "session-1", + }); + + await expect(apiKeyCaller().superPassword.extend()).rejects.toMatchObject({ + code: "FORBIDDEN", + }); + await expect(browserCaller().superPassword.extend()).resolves.toMatchObject( + { + expiresAt: expect.any(Date), + }, + ); + }); + + it("closes access and needs the super password to turn it off", async () => { + await setSuperPassword(); + await openSuperSession(); + + await browserCaller().superPassword.close(); + await expect(service.getSuperSessionState(owner.id)).resolves.toMatchObject( + { + active: false, + }, + ); + + await expect( + browserCaller().superPassword.disable({ password: "wrong-password" }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + await browserCaller().superPassword.disable({ + password: "super-password-1", + }); + await expect(service.getSuperSessionState(owner.id)).resolves.toMatchObject( + { + isSet: false, + }, + ); + }); + + it("closes access from the email link without signing in", async () => { + await setSuperPassword(); + await openSuperSession(); + const { token } = await service.createSuperPasswordToken({ + userId: owner.id, + type: "lock", + }); + const anonymous = testRouter.createCaller({ + db: {}, + req: { headers: {} }, + res: {}, + user: null, + session: null, + } as never); + + await expect( + anonymous.superPassword.lockWithToken({ token }), + ).resolves.toEqual({ closed: true }); + await expect( + anonymous.superPassword.lockWithToken({ token }), + ).resolves.toEqual({ closed: false }); + await expect(service.getSuperSessionState(owner.id)).resolves.toMatchObject( + { + active: false, + }, + ); + }); +}); + +describe("login password changes", () => { + it("rejects a new login password equal to the super password", async () => { + const bcrypt = await import("bcrypt"); + fake.seed(schema.account, { + id: "account-1", + accountId: "account-1", + providerId: "credential", + userId: owner.id, + password: await bcrypt.hash("login-password-1", 4), + createdAt: new Date(), + updatedAt: new Date(), + }); + await setSuperPassword(); + await openSuperSession(); + + await expect( + userRouter + .createCaller({ + db: {}, + req: { headers: {} }, + res: {}, + user: owner, + session: { + id: "session-1", + userId: owner.id, + activeOrganizationId: "org-1", + authMethod: "session", + }, + } as never) + .update({ + email: owner.email, + currentPassword: "login-password-1", + password: "super-password-1", + }), + ).rejects.toMatchObject({ + code: "BAD_REQUEST", + message: "The login password must differ from your super password", + }); + expect( + await bcrypt.compare( + "login-password-1", + String(fake.rows(schema.account)[0]?.password), + ), + ).toBe(true); + }); +}); diff --git a/apps/dokploy/__test__/security/super-session-procedure-access.test.ts b/apps/dokploy/__test__/security/super-session-procedure-access.test.ts new file mode 100644 index 00000000000..32661386bd0 --- /dev/null +++ b/apps/dokploy/__test__/security/super-session-procedure-access.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it } from "vitest"; +import { appRouter } from "@/server/api/root"; +import { PROCEDURE_ACCESS } from "@/server/api/utils/procedure-access"; + +type ProcedureDef = { _def: { type: "query" | "mutation" | "subscription" } }; + +const procedures = Object.entries( + ( + appRouter as unknown as { + _def: { procedures: Record }; + } + )._def.procedures, +).map(([path, procedure]) => ({ path, type: procedure._def.type })); + +const classified = (path: string) => PROCEDURE_ACCESS[path]; + +const DATABASES = ["postgres", "mysql", "mariadb", "mongo", "redis", "libsql"]; + +const REQUIRED_DANGEROUS = [ + "application.revealEnvironment", + "compose.revealEnvironment", + ...DATABASES.map((database) => `${database}.revealEnvironment`), + "docker.getConfig", + "settings.readTraefikEnv", + "cluster.addManager", + "cluster.addWorker", + "backup.manualBackupWebServer", + "backup.restoreBackupWithLogs", + "volumeBackups.restoreVolumeBackupWithLogs", + "user.createApiKey", + "user.deleteApiKey", + "user.deleteAccount", + "user.remove", + "user.assignPermissions", + "user.sendInvitation", + "organization.inviteMember", + "organization.removeInvitation", + "organization.updateMemberRole", + "customRole.create", + "customRole.update", + "customRole.remove", + "sso.register", + "sso.update", + "sso.deleteProvider", + "scim.createConnection", + "scim.rotateCredential", + "settings.updateEnforceSSO", + "notification.createEmail", + "notification.updateEmail", + "notification.createResend", + "notification.updateResend", + "notification.remove", + "settings.updateTraefikConfig", + "settings.writeTraefikEnv", + "settings.updateTraefikPorts", + "settings.updateTraefikFile", + "settings.updateWebServerTraefikConfig", + "settings.assignDomainServer", + "settings.updateServer", + "settings.reloadServer", + "settings.saveSSHPrivateKey", + "settings.cleanSSHPrivateKey", + "sshKey.create", + "sshKey.update", + "sshKey.remove", + "server.create", + "server.update", + "server.remove", + "project.remove", + "environment.remove", + "application.delete", + "compose.delete", + ...DATABASES.map((database) => `${database}.remove`), + "destination.remove", + "registry.remove", + "gitProvider.remove", + "certificates.remove", + "dockerVolume.removeVolume", +]; + +const REQUIRED_CONDITIONAL = [ + "user.update", + "schedule.create", + "schedule.update", + "schedule.runManually", + "backup.create", + "backup.update", +]; + +describe("super session procedure classification", () => { + it("classifies every procedure of the app router", () => { + const missing = procedures + .map(({ path }) => path) + .filter((path) => classified(path) === undefined); + + expect( + missing, + "Classify new procedures in server/api/utils/procedure-access.ts as read, write or dangerous", + ).toEqual([]); + }); + + it("has no entries for procedures that no longer exist", () => { + const paths = new Set(procedures.map(({ path }) => path)); + expect( + Object.keys(PROCEDURE_ACCESS).filter((path) => !paths.has(path)), + ).toEqual([]); + }); + + it("never classifies a mutation or subscription as a read", () => { + const weakened = procedures + .filter(({ type }) => type !== "query") + .filter(({ path }) => classified(path) === "read") + .map(({ path }) => path); + + expect(weakened).toEqual([]); + }); + + it("marks secret reveals, access changes, host settings and deletions as dangerous", () => { + expect( + REQUIRED_DANGEROUS.filter((path) => classified(path) !== "dangerous"), + ).toEqual([]); + expect( + REQUIRED_CONDITIONAL.filter((path) => { + const access = classified(path); + return typeof access !== "object" || !("dangerousWhen" in access); + }), + ).toEqual([]); + }); + + it("keeps the status query readable through API keys", () => { + expect(classified("superPassword.status")).toBe("read"); + }); +}); diff --git a/apps/dokploy/__test__/settings/docker-server-boundary.test.ts b/apps/dokploy/__test__/settings/docker-server-boundary.test.ts index f83350b3e22..f7633d654f9 100644 --- a/apps/dokploy/__test__/settings/docker-server-boundary.test.ts +++ b/apps/dokploy/__test__/settings/docker-server-boundary.test.ts @@ -1,10 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ + assertLocalHostAccess: vi.fn(), audit: vi.fn(), checkGPUStatus: vi.fn(), checkPermission: vi.fn(), checkPortInUse: vi.fn(), + checkProtectedResourceAccess: vi.fn(), checkPostgresHealth: vi.fn(), checkRedisHealth: vi.fn(), checkTraefikHealth: vi.fn(), @@ -17,6 +19,7 @@ const mocks = vi.hoisted(() => ({ cleanupSystem: vi.fn(), cleanupVolumes: vi.fn(), execAsync: vi.fn(), + filterProtectedTraefikEntries: vi.fn(), findServerById: vi.fn(), generateOpenApiDocument: vi.fn(), getAccessibleServerIds: vi.fn(), @@ -78,6 +81,7 @@ vi.mock("@dokploy/server", () => ({ cleanupSystem: mocks.cleanupSystem, cleanupVolumes: mocks.cleanupVolumes, execAsync: mocks.execAsync, + filterProtectedTraefikEntries: mocks.filterProtectedTraefikEntries, findServerById: mocks.findServerById, getAccessibleServerIds: mocks.getAccessibleServerIds, getAgentHitsUpdateCommand: mocks.getAgentHitsUpdateCommand, @@ -124,6 +128,13 @@ vi.mock("@dokploy/server/services/permission", () => ({ checkPermission: mocks.checkPermission, })); +vi.mock("@dokploy/server/services/super-password", async (importOriginal) => ({ + ...(await importOriginal< + typeof import("@dokploy/server/services/super-password") + >()), + checkProtectedResourceAccess: mocks.checkProtectedResourceAccess, +})); + vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ hasValidLicense: mocks.hasValidLicense, })); @@ -141,6 +152,10 @@ vi.mock("@/server/api/utils/audit", () => ({ audit: mocks.audit, })); +vi.mock("@/server/api/utils/local-host-access", () => ({ + assertLocalHostAccess: mocks.assertLocalHostAccess, +})); + vi.mock("@/server/queues/concurrency", () => ({ assertBuildsConcurrencyAllowed: vi.fn(), })); @@ -159,8 +174,14 @@ vi.mock("../../server/api/root", () => ({ })); const { settingsRouter } = await import("../../server/api/routers/settings"); - -const createCaller = (role: "owner" | "admin" | "member" = "admin") => +const { getSuperSessionDenial } = await import( + "@dokploy/server/services/super-password" +); + +const createCaller = ( + role: "owner" | "admin" | "member" = "admin", + authMethod?: "api-key", +) => settingsRouter.createCaller({ db: {}, req: { @@ -173,6 +194,7 @@ const createCaller = (role: "owner" | "admin" | "member" = "admin") => session: { userId: "user-1", activeOrganizationId: "org-1", + ...(authMethod ? { authMethod } : {}), }, user: { id: "user-1", @@ -493,3 +515,157 @@ describe("settings Docker server boundary", () => { }); }); }); + +describe("settings Traefik file access", () => { + const localTraefikPath = "/etc/dokploy/traefik"; + + beforeEach(() => { + vi.clearAllMocks(); + mocks.checkPermission.mockResolvedValue(undefined); + mocks.assertLocalHostAccess.mockResolvedValue(undefined); + mocks.getAccessibleServerIds.mockResolvedValue(new Set(["server-1"])); + mocks.paths.mockReturnValue({ + MAIN_TRAEFIK_PATH: localTraefikPath, + }); + mocks.readConfigInPath.mockResolvedValue("http: {}"); + mocks.readDirectory.mockResolvedValue([]); + mocks.filterProtectedTraefikEntries.mockImplementation( + (entries: unknown[]) => entries, + ); + mocks.checkProtectedResourceAccess.mockResolvedValue( + "super-session-required", + ); + }); + + it("requires local host access for local Traefik files", async () => { + mocks.assertLocalHostAccess.mockRejectedValue( + Object.assign(new Error("Local host operations require owner or admin"), { + code: "UNAUTHORIZED", + }), + ); + const caller = createCaller("member"); + + await expect(caller.readDirectories({})).rejects.toThrow( + "Local host operations require owner or admin", + ); + await expect( + caller.readTraefikFile({ + path: `${process.cwd()}/.docker/traefik/dynamic/app.yml`, + }), + ).rejects.toThrow("Local host operations require owner or admin"); + await expect( + caller.updateTraefikFile({ + path: `${localTraefikPath}/dynamic/app.yml`, + traefikConfig: "http: {}", + }), + ).rejects.toThrow("Local host operations require owner or admin"); + + expect(mocks.assertLocalHostAccess).toHaveBeenCalledTimes(3); + expect(mocks.readDirectory).not.toHaveBeenCalled(); + expect(mocks.readConfigInPath).not.toHaveBeenCalled(); + expect(mocks.writeTraefikConfigInPath).not.toHaveBeenCalled(); + }); + + it("hides protected entries from remote Traefik directory listings", async () => { + const listing = [ + { id: `${localTraefikPath}/dynamic/acme.json`, name: "acme.json" }, + ]; + mocks.readDirectory.mockResolvedValue(listing); + mocks.filterProtectedTraefikEntries.mockReturnValue([]); + + await expect( + createCaller().readDirectories({ serverId: "server-1" }), + ).resolves.toEqual([]); + + expect(mocks.filterProtectedTraefikEntries).toHaveBeenCalledWith( + listing, + "server-1", + ); + expect(mocks.assertLocalHostAccess).not.toHaveBeenCalled(); + }); + + describe("TLS files behind the super session", () => { + const acmePath = `${process.cwd()}/.docker/traefik/dynamic/acme.json`; + const keyPath = `${process.cwd()}/.docker/traefik/dynamic/certificates/site/key.key`; + + it("denies TLS files while the super session is closed", async () => { + const caller = createCaller("owner"); + + for (const run of [ + () => caller.readTraefikFile({ path: acmePath }), + () => caller.updateTraefikFile({ path: keyPath, traefikConfig: "x" }), + ]) { + const error = await run().then( + () => null, + (caught: unknown) => caught, + ); + expect(error).toMatchObject({ code: "FORBIDDEN" }); + expect(getSuperSessionDenial(error)).toBe("super-session-required"); + } + expect(mocks.readConfigInPath).not.toHaveBeenCalled(); + expect(mocks.writeTraefikConfigInPath).not.toHaveBeenCalled(); + }); + + it("opens TLS files for owners and admins while the super session is open", async () => { + mocks.checkProtectedResourceAccess.mockResolvedValue(null); + const caller = createCaller("owner"); + + await caller.readTraefikFile({ path: acmePath }); + expect(mocks.readConfigInPath).toHaveBeenCalledWith(acmePath, undefined, { + allowProtected: true, + }); + + await caller.updateTraefikFile({ path: keyPath, traefikConfig: "x" }); + expect(mocks.writeTraefikConfigInPath).toHaveBeenCalledWith( + keyPath, + "x", + undefined, + { allowProtected: true }, + ); + expect(mocks.checkProtectedResourceAccess).toHaveBeenCalledWith({ + userId: "user-1", + viaApiKey: false, + }); + }); + + it("never opens TLS files through an API key", async () => { + mocks.checkProtectedResourceAccess.mockImplementation( + async ({ viaApiKey }: { viaApiKey: boolean }) => + viaApiKey ? "browser-session-required" : null, + ); + + await expect( + createCaller("owner", "api-key").readTraefikFile({ path: acmePath }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }); + expect(mocks.readConfigInPath).not.toHaveBeenCalled(); + }); + + it("does not involve the super session for regular Traefik files", async () => { + const appPath = `${process.cwd()}/.docker/traefik/dynamic/app.yml`; + + await createCaller("owner").readTraefikFile({ path: appPath }); + + expect(mocks.checkProtectedResourceAccess).not.toHaveBeenCalled(); + expect(mocks.readConfigInPath).toHaveBeenCalledWith(appPath, undefined, { + allowProtected: false, + }); + }); + + it("lists TLS files only while the super session is open", async () => { + const listing = [{ id: acmePath, name: "acme.json" }]; + mocks.readDirectory.mockResolvedValue(listing); + mocks.filterProtectedTraefikEntries.mockReturnValue([]); + + await expect(createCaller("owner").readDirectories({})).resolves.toEqual( + [], + ); + + mocks.checkProtectedResourceAccess.mockResolvedValue(null); + mocks.filterProtectedTraefikEntries.mockClear(); + await expect(createCaller("owner").readDirectories({})).resolves.toEqual( + listing, + ); + expect(mocks.filterProtectedTraefikEntries).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/dokploy/__test__/traefik/traefik-file-boundary.test.ts b/apps/dokploy/__test__/traefik/traefik-file-boundary.test.ts index 4abc537497a..f37ea1d4426 100644 --- a/apps/dokploy/__test__/traefik/traefik-file-boundary.test.ts +++ b/apps/dokploy/__test__/traefik/traefik-file-boundary.test.ts @@ -17,13 +17,20 @@ vi.mock("@dokploy/server/utils/process/execAsync", () => ({ writeFileRemote: mocks.writeFileRemote, })); -const { readConfigInPath, writeTraefikConfigInPath, writeTraefikConfigRemote } = - await import("@dokploy/server/utils/traefik/application"); +const { + filterProtectedTraefikEntries, + readConfigInPath, + writeTraefikConfigInPath, + writeTraefikConfigRemote, +} = await import("@dokploy/server/utils/traefik/application"); describe("Traefik file path boundary", () => { beforeEach(() => { vi.clearAllMocks(); mocks.paths.mockImplementation((remote?: boolean) => ({ + CERTIFICATES_PATH: remote + ? "/etc/dokploy/traefik/dynamic/certificates" + : "/var/lib/dokploy/traefik/dynamic/certificates", DYNAMIC_TRAEFIK_PATH: remote ? "/etc/dokploy/traefik/dynamic" : "/var/lib/dokploy/traefik/dynamic", @@ -69,6 +76,109 @@ describe("Traefik file path boundary", () => { expect(mocks.execAsyncRemote).not.toHaveBeenCalled(); }); + it.each([ + ["/var/lib/dokploy/traefik/dynamic/acme.json", undefined], + ["/var/lib/dokploy/traefik/dynamic/certificates", undefined], + [ + "/var/lib/dokploy/traefik/dynamic/certificates/cert-1/chain.crt", + undefined, + ], + ["/var/lib/dokploy/traefik/dynamic/custom/privkey.key", undefined], + [ + "/var/lib/dokploy/traefik/dynamic/./certificates/../certificates/cert-1/privkey.key", + undefined, + ], + ["/etc/dokploy/traefik/dynamic/acme.json", "server-1"], + ["/etc/dokploy/traefik/dynamic/certificates/cert-1/chain.crt", "server-1"], + ["/etc/dokploy/traefik/tls/site.KEY", "server-1"], + ])( + "rejects reads and writes of TLS secret file %s", + async (filePath, serverId) => { + await expect(readConfigInPath(filePath, serverId)).rejects.toThrow( + "Access to this Traefik file is not allowed", + ); + await expect( + writeTraefikConfigInPath(filePath, "{}", serverId), + ).rejects.toThrow("Access to this Traefik file is not allowed"); + + expect(mocks.execAsyncRemote).not.toHaveBeenCalled(); + expect(mocks.writeFileRemote).not.toHaveBeenCalled(); + }, + ); + + it("opens TLS secret files only when the caller passed the super session check", async () => { + const acmePath = "/etc/dokploy/traefik/dynamic/acme.json"; + mocks.execAsyncRemote.mockResolvedValue({ stdout: "{}" }); + + await expect( + readConfigInPath(acmePath, "server-1", { allowProtected: false }), + ).rejects.toThrow("Access to this Traefik file is not allowed"); + await expect( + readConfigInPath(acmePath, "server-1", { allowProtected: true }), + ).resolves.toBe("{}"); + + await writeTraefikConfigInPath(acmePath, "{}", "server-1", { + allowProtected: true, + }); + expect(mocks.writeFileRemote).toHaveBeenCalledWith( + "server-1", + acmePath, + "{}", + ); + + await expect( + readConfigInPath("/etc/dokploy/app/.env", "server-1", { + allowProtected: true, + }), + ).rejects.toThrow("Invalid Traefik config path"); + }); + + it("hides TLS secret files from Traefik directory listings", () => { + const root = "/etc/dokploy/traefik"; + const listing = [ + { id: `${root}/traefik.yml`, name: "traefik.yml", type: "file" }, + { + id: `${root}/dynamic`, + name: "dynamic", + type: "directory", + children: [ + { id: `${root}/dynamic/acme.json`, name: "acme.json", type: "file" }, + { id: `${root}/dynamic/app.yml`, name: "app.yml", type: "file" }, + { + id: `${root}/dynamic/certificates`, + name: "certificates", + type: "directory", + children: [ + { + id: `${root}/dynamic/certificates/cert-1`, + name: "cert-1", + type: "directory", + children: [], + }, + ], + }, + { + id: `${root}/dynamic/custom.key`, + name: "custom.key", + type: "file", + }, + ], + }, + ]; + + expect(filterProtectedTraefikEntries(listing, "server-1")).toEqual([ + { id: `${root}/traefik.yml`, name: "traefik.yml", type: "file" }, + { + id: `${root}/dynamic`, + name: "dynamic", + type: "directory", + children: [ + { id: `${root}/dynamic/app.yml`, name: "app.yml", type: "file" }, + ], + }, + ]); + }); + it("quotes remote Traefik file paths before shell execution", async () => { const remotePath = "/etc/dokploy/traefik/dynamic/app'$(id).yml"; diff --git a/apps/dokploy/__test__/wss/terminal-super-session.test.ts b/apps/dokploy/__test__/wss/terminal-super-session.test.ts new file mode 100644 index 00000000000..66ae272bf41 --- /dev/null +++ b/apps/dokploy/__test__/wss/terminal-super-session.test.ts @@ -0,0 +1,211 @@ +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { WebSocket } from "ws"; + +const mocks = vi.hoisted(() => ({ + canAccessDockerOverWss: vi.fn(), + canAccessDockerTerminalWebSocket: vi.fn(), + canAccessServerTerminalWebSocket: vi.fn(), + canAccessTerminalOverWss: vi.fn(), + setupLocalServerSSHKey: vi.fn(), + spawn: vi.fn(), + validateRequest: vi.fn(), +})); + +vi.mock("@dokploy/server", () => ({ + IS_CLOUD: false, + execAsync: vi.fn(), + findServerById: vi.fn(), + resolveTrustedOriginsForAuthRequest: vi.fn(async () => []), + validateRequest: mocks.validateRequest, +})); + +vi.mock("@dokploy/server/db", async () => { + const schema = await import("@dokploy/server/db/schema"); + const { createFakeDrizzleDb } = await import("../helpers/fake-drizzle-db"); + const fake = createFakeDrizzleDb({ + superPassword: schema.superPassword, + superSession: schema.superSession, + account: schema.account, + }); + return { db: fake.db, fake }; +}); + +vi.mock("../../server/wss/authorize", () => ({ + canAccessDockerOverWss: mocks.canAccessDockerOverWss, + canAccessTerminalOverWss: mocks.canAccessTerminalOverWss, +})); + +vi.mock("../../server/wss/server-permission", () => ({ + canAccessServerTerminalWebSocket: mocks.canAccessServerTerminalWebSocket, +})); + +vi.mock("../../server/wss/docker-permission", () => ({ + canAccessDockerTerminalWebSocket: mocks.canAccessDockerTerminalWebSocket, +})); + +vi.mock("../../server/wss/utils", async (importOriginal) => ({ + ...((await importOriginal< + typeof import("../../server/wss/utils") + >()) as object), + setupLocalServerSSHKey: mocks.setupLocalServerSSHKey, +})); + +vi.mock("node-pty", () => ({ spawn: mocks.spawn })); + +const { fake } = (await import("@dokploy/server/db")) as unknown as { + fake: ReturnType< + typeof import("../helpers/fake-drizzle-db")["createFakeDrizzleDb"] + >; +}; +const service = await import("@dokploy/server/services/super-password"); +const { setupTerminalWebSocketServer } = await import( + "../../server/wss/terminal" +); +const { setupDockerContainerTerminalWebSocketServer } = await import( + "../../server/wss/docker-container-terminal" +); + +const listen = async (server: http.Server) => + new Promise((resolve) => { + server.listen(0, "127.0.0.1", () => { + resolve((server.address() as AddressInfo).port); + }); + }); + +const closeServer = async (server: http.Server) => + new Promise((resolve) => { + server.close(() => resolve()); + }); + +const openAndWaitForClose = async (port: number, path: string) => + new Promise<{ code: number; messages: string[] }>((resolve, reject) => { + const messages: string[] = []; + const timeout = setTimeout(() => { + reject(new Error("WebSocket close timeout")); + }, 3000); + const ws = new WebSocket(`ws://127.0.0.1:${port}${path}`); + ws.on("message", (data) => messages.push(data.toString())); + ws.on("close", (code) => { + clearTimeout(timeout); + resolve({ code, messages }); + }); + ws.on("error", reject); + }); + +const browserSession = { + user: { id: "user-1" }, + session: { + id: "session-1", + activeOrganizationId: "org-1", + authMethod: "session", + }, +}; +const apiKeySession = { + user: { id: "user-1" }, + session: { activeOrganizationId: "org-1", authMethod: "api-key" }, +}; + +describe("terminal WebSockets need an open super session", () => { + let server: http.Server; + let port: number; + + beforeEach(async () => { + fake.reset(); + vi.clearAllMocks(); + mocks.canAccessTerminalOverWss.mockResolvedValue(true); + mocks.canAccessServerTerminalWebSocket.mockResolvedValue(true); + mocks.canAccessDockerOverWss.mockResolvedValue(true); + mocks.canAccessDockerTerminalWebSocket.mockResolvedValue(true); + mocks.setupLocalServerSSHKey.mockResolvedValue(null); + mocks.spawn.mockReturnValue({ + onData: vi.fn(), + onExit: (callback: (event: { exitCode: number }) => void) => + setTimeout(() => callback({ exitCode: 0 }), 10), + kill: vi.fn(), + resize: vi.fn(), + write: vi.fn(), + }); + mocks.validateRequest.mockResolvedValue(browserSession); + + server = http.createServer(); + setupTerminalWebSocketServer(server); + setupDockerContainerTerminalWebSocketServer(server); + port = await listen(server); + }); + + afterEach(async () => { + await closeServer(server); + }); + + const hostTerminal = "/terminal?serverId=local&port=22&username=root"; + const containerTerminal = + "/docker-container-terminal?containerId=abc123def456"; + + it("keeps today's behaviour for users without a super password", async () => { + await openAndWaitForClose(port, hostTerminal); + expect(mocks.setupLocalServerSSHKey).toHaveBeenCalled(); + + await openAndWaitForClose(port, containerTerminal); + expect(mocks.spawn).toHaveBeenCalled(); + }); + + it("rejects host and container terminals while the super session is closed", async () => { + await service.setSuperPassword({ + userId: "user-1", + password: "super-password-1", + }); + + for (const path of [hostTerminal, containerTerminal]) { + const result = await openAndWaitForClose(port, path); + expect(result.code, path).toBe(4003); + expect(result.messages.join(""), path).toContain( + service.SUPER_SESSION_MESSAGES["super-session-required"], + ); + } + expect(mocks.setupLocalServerSSHKey).not.toHaveBeenCalled(); + expect(mocks.spawn).not.toHaveBeenCalled(); + }); + + it("opens terminals for the browser session while the super session is open", async () => { + await service.setSuperPassword({ + userId: "user-1", + password: "super-password-1", + }); + await service.openSuperSession({ + userId: "user-1", + sessionId: "session-1", + }); + + const host = await openAndWaitForClose(port, hostTerminal); + expect(host.code).not.toBe(4003); + expect(mocks.setupLocalServerSSHKey).toHaveBeenCalled(); + + const container = await openAndWaitForClose(port, containerTerminal); + expect(container.code).not.toBe(4003); + expect(mocks.spawn).toHaveBeenCalled(); + }); + + it("never opens terminals for API keys of a user with a super password", async () => { + await service.setSuperPassword({ + userId: "user-1", + password: "super-password-1", + }); + await service.openSuperSession({ + userId: "user-1", + sessionId: "session-1", + }); + mocks.validateRequest.mockResolvedValue(apiKeySession); + + for (const path of [hostTerminal, containerTerminal]) { + const result = await openAndWaitForClose(port, path); + expect(result.code, path).toBe(4003); + expect(result.messages.join(""), path).toContain( + service.SUPER_SESSION_MESSAGES["browser-session-required"], + ); + } + expect(mocks.setupLocalServerSSHKey).not.toHaveBeenCalled(); + expect(mocks.spawn).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 093f1666445..f5ba8244ae1 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -59,6 +59,7 @@ const notificationBaseSchema = z.object({ dokployRestart: z.boolean().default(false), dockerCleanup: z.boolean().default(false), serverThreshold: z.boolean().default(false), + superPassword: z.boolean().default(false), }); export const notificationSchema = z.discriminatedUnion("type", [ @@ -364,6 +365,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, type: notification.notificationType, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "telegram") { form.reset({ @@ -380,6 +382,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "discord") { form.reset({ @@ -395,6 +398,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "email") { form.reset({ @@ -414,6 +418,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "resend") { form.reset({ @@ -430,6 +435,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "gotify") { form.reset({ @@ -447,6 +453,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "ntfy") { form.reset({ @@ -464,6 +471,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "mattermost") { form.reset({ @@ -480,6 +488,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "lark") { form.reset({ @@ -494,6 +503,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { dockerCleanup: notification.dockerCleanup, volumeBackup: notification.volumeBackup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "teams") { form.reset({ @@ -508,6 +518,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "custom") { form.reset({ @@ -530,6 +541,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { volumeBackup: notification.volumeBackup, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } else if (notification.notificationType === "pushover") { form.reset({ @@ -548,6 +560,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, + superPassword: notification.superPassword, }); } } else { @@ -580,6 +593,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { volumeBackup, dockerCleanup, serverThreshold, + superPassword, } = data; let promise: Promise | null = null; if (data.type === "slack") { @@ -597,6 +611,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { slackId: notification?.slackId || "", notificationId: notificationId || "", serverThreshold: serverThreshold, + superPassword: superPassword, }); } else if (data.type === "telegram") { promise = telegramMutation.mutateAsync({ @@ -614,6 +629,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", telegramId: notification?.telegramId || "", serverThreshold: serverThreshold, + superPassword: superPassword, }); } else if (data.type === "discord") { promise = discordMutation.mutateAsync({ @@ -630,6 +646,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", discordId: notification?.discordId || "", serverThreshold: serverThreshold, + superPassword: superPassword, }); } else if (data.type === "email") { promise = emailMutation.mutateAsync({ @@ -650,6 +667,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", emailId: notification?.emailId || "", serverThreshold: serverThreshold, + superPassword: superPassword, }); } else if (data.type === "resend") { promise = resendMutation.mutateAsync({ @@ -667,6 +685,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", resendId: notification?.resendId || "", serverThreshold: serverThreshold, + superPassword: superPassword, }); } else if (data.type === "gotify") { promise = gotifyMutation.mutateAsync({ @@ -683,6 +702,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { dockerCleanup: dockerCleanup, decoration: data.decoration, serverThreshold: serverThreshold, + superPassword: superPassword, notificationId: notificationId || "", gotifyId: notification?.gotifyId || "", }); @@ -701,6 +721,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: data.name, dockerCleanup: dockerCleanup, serverThreshold: serverThreshold, + superPassword: superPassword, notificationId: notificationId || "", ntfyId: notification?.ntfyId || "", }); @@ -720,6 +741,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", mattermostId: notification?.mattermostId || "", serverThreshold: serverThreshold, + superPassword: superPassword, }); } else if (data.type === "lark") { promise = larkMutation.mutateAsync({ @@ -735,6 +757,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", larkId: notification?.larkId || "", serverThreshold: serverThreshold, + superPassword: superPassword, }); } else if (data.type === "teams") { promise = teamsMutation.mutateAsync({ @@ -750,6 +773,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", teamsId: notification?.teamsId || "", serverThreshold: serverThreshold, + superPassword: superPassword, }); } else if (data.type === "custom") { // Convert headers array to object @@ -776,6 +800,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: data.name, dockerCleanup: dockerCleanup, serverThreshold: serverThreshold, + superPassword: superPassword, notificationId: notificationId || "", customId: notification?.customId || "", }); @@ -799,6 +824,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: data.name, dockerCleanup: dockerCleanup, serverThreshold: serverThreshold, + superPassword: superPassword, notificationId: notificationId || "", pushoverId: notification?.pushoverId || "", }); @@ -1971,6 +1997,28 @@ export const HandleNotifications = ({ notificationId }: Props) => { /> )} + ( + +
+ Super password + + Security alerts and recovery links for the super + password + +
+ + + +
+ )} + /> + {isCloud && ( { + if (ms < 60_000) { + return "less than 1 min"; + } + const totalMinutes = Math.floor(ms / 60_000); + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + return hours > 0 ? `${hours} h ${minutes} min` : `${minutes} min`; +}; + +const errorMessage = (error: unknown, fallback: string) => + error instanceof Error ? error.message : fallback; + +const PasswordField = ({ + id, + label, + value, + onChange, + autoFocus, +}: { + id: string; + label: string; + value: string; + onChange: (value: string) => void; + autoFocus?: boolean; +}) => ( +
+ + onChange(event.target.value)} + /> +
+); + +const HintField = ({ + value, + onChange, +}: { + value: string; + onChange: (value: string) => void; +}) => ( +
+ + onChange(event.target.value)} + /> +
+); + +const validateNewPassword = (password: string, confirm: string) => { + if (password.length < 8) { + return "The super password must be at least 8 characters long"; + } + if (password !== confirm) { + return "The passwords do not match"; + } + return null; +}; + +const SetSuperPasswordForm = ({ onDone }: { onDone: () => void }) => { + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [hint, setHint] = useState(""); + const setSuperPassword = api.superPassword.set.useMutation(); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const invalid = validateNewPassword(password, confirm); + if (invalid) { + toast.error(invalid); + return; + } + try { + await setSuperPassword.mutateAsync({ password, hint: hint || null }); + toast.success("Super password set"); + onDone(); + } catch (error) { + toast.error(errorMessage(error, "Error setting the super password")); + } + }; + + return ( +
+ + +
+ +
+
+ +
+ + ); +}; + +const ChangeSuperPasswordDialog = ({ onDone }: { onDone: () => void }) => { + const [open, setOpen] = useState(false); + const [currentPassword, setCurrentPassword] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [hint, setHint] = useState(""); + const change = api.superPassword.change.useMutation(); + + const reset = () => { + setCurrentPassword(""); + setPassword(""); + setConfirm(""); + setHint(""); + change.reset(); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const invalid = validateNewPassword(password, confirm); + if (invalid) { + toast.error(invalid); + return; + } + try { + await change.mutateAsync({ + currentPassword, + password, + hint: hint || null, + }); + toast.success("Super password changed"); + setOpen(false); + reset(); + onDone(); + } catch { + onDone(); + } + }; + + return ( + { + setOpen(next); + if (!next) { + reset(); + } + }} + > + + + + + + Change super password + + Enter the current super password, then the new one. It must differ + from your login password. + + +
+ {change.isError && ( + {change.error.message} + )} + + + + + + + + +
+
+ ); +}; + +const DisableSuperPasswordDialog = ({ onDone }: { onDone: () => void }) => { + const [open, setOpen] = useState(false); + const [password, setPassword] = useState(""); + const disable = api.superPassword.disable.useMutation(); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + await disable.mutateAsync({ password }); + toast.success("Super password turned off"); + setOpen(false); + setPassword(""); + onDone(); + } catch { + onDone(); + } + }; + + return ( + { + setOpen(next); + if (!next) { + setPassword(""); + disable.reset(); + } + }} + > + + + + + + Turn off the super password + + API keys get write access again and dangerous actions no longer ask + for a second password. Open super password access is closed. + + +
+ {disable.isError && ( + {disable.error.message} + )} + + + + + +
+
+ ); +}; + +const CHANNEL_TYPE_LABELS: Record = { + slack: "Slack", + telegram: "Telegram", + discord: "Discord", + email: "Email", + resend: "Resend", + gotify: "Gotify", + ntfy: "ntfy", + mattermost: "Mattermost", + pushover: "Pushover", + custom: "Webhook", + lark: "Lark", + teams: "Teams", +}; + +const RecoveryChannelPicker = ({ + channels, +}: { + channels: { + notificationId: string; + name: string; + notificationType: string; + }[]; +}) => { + const [notificationId, setNotificationId] = useState(""); + const requestReset = api.superPassword.requestReset.useMutation(); + const selected = + channels.find((channel) => channel.notificationId === notificationId) ?? + channels[0]; + + if (channels.length === 0) { + return ( +

+ No recovery channel: enable 'Super password' on a notification, or use + SSH. +

+ ); + } + + const handleSend = async () => { + if (!selected) { + return; + } + try { + await requestReset.mutateAsync({ + notificationId: selected.notificationId, + }); + toast.success(`Reset link sent to ${selected.name}`); + } catch (error) { + toast.error(errorMessage(error, "Error sending the reset link")); + } + }; + + return ( +
+ +
+ + +
+

+ The link works once for 30 minutes and only while you are signed in as + this user. +

+
+ ); +}; + +export const SuperPasswordCard = () => { + const utils = api.useUtils(); + const { data: status, isPending } = api.superPassword.status.useQuery(); + const [password, setPassword] = useState(""); + const [now, setNow] = useState(() => Date.now()); + const unlock = api.superPassword.unlock.useMutation(); + const extend = api.superPassword.extend.useMutation(); + const close = api.superPassword.close.useMutation(); + + const expiresAt = status?.expiresAt + ? new Date(status.expiresAt).getTime() + : null; + const remaining = expiresAt ? expiresAt - now : 0; + + useEffect(() => { + if (!status?.active) { + return; + } + const timer = setInterval(() => setNow(Date.now()), 15_000); + return () => clearInterval(timer); + }, [status?.active]); + + useEffect(() => { + if (status?.active && expiresAt && remaining <= 0) { + void utils.superPassword.status.invalidate(); + } + }, [status?.active, expiresAt, remaining, utils]); + + const refresh = () => { + setNow(Date.now()); + return utils.superPassword.status.invalidate(); + }; + + if (isPending) { + return ( +
+ Loading... + +
+ ); + } + + if (!status || (!status.canManage && !status.isSet)) { + return null; + } + + const handleUnlock = async (event: React.FormEvent) => { + event.preventDefault(); + try { + await unlock.mutateAsync({ password }); + setPassword(""); + toast.success("Access open for 24 hours"); + } catch (error) { + toast.error(errorMessage(error, "Error opening access")); + } finally { + await refresh(); + } + }; + + const handleExtend = async () => { + try { + await extend.mutateAsync(); + toast.success("Access extended for 24 hours"); + } catch (error) { + toast.error(errorMessage(error, "Error extending access")); + } finally { + await refresh(); + } + }; + + const handleClose = async () => { + try { + await close.mutateAsync(); + toast.success("Super password access closed"); + } catch (error) { + toast.error(errorMessage(error, "Error closing access")); + } finally { + await refresh(); + } + }; + + const lockedUntil = status.lockedUntil ? new Date(status.lockedUntil) : null; + const isWarning = status.active && remaining < WARN_REMAINING_MS; + + return ( +
+ +
+ +
+ + + Super password + + + A second password for dangerous actions and API key writes. + Entering it opens access for 24 hours. + +
+ {!status.isSet ? ( + Not set + ) : status.active ? ( + Access open + ) : ( + Access closed + )} +
+ + + {!status.isSet ? ( + <> + + Not set. Until you set a super password, API keys can write + and dangerous actions (revealing secrets, terminals, users and + access, host settings, deletions) need no second password. + + void refresh()} /> + + ) : ( + <> + {lockedUntil && ( + + Locked after too many wrong attempts until{" "} + {lockedUntil.toLocaleTimeString()}. + + )} + + {status.active ? ( +
+ } + > + Access open: {formatSuperSessionRemaining(remaining)} left + {isWarning && + " — extend it to keep API key writes working"} + +
+ + +
+
+ ) : ( +
+ + Access closed: API keys are read-only and dangerous + actions ask for the super password. + +
+ +
+ setPassword(event.target.value)} + /> + +
+ {status.hint && ( +

+ Hint: {status.hint} +

+ )} +
+
+ )} + +
+ void refresh()} /> + void refresh()} /> +
+ + + )} +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/shared/super-password-unlock-dialog.tsx b/apps/dokploy/components/shared/super-password-unlock-dialog.tsx new file mode 100644 index 00000000000..3cdd94b9ad3 --- /dev/null +++ b/apps/dokploy/components/shared/super-password-unlock-dialog.tsx @@ -0,0 +1,143 @@ +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + registerSuperSessionPrompt, + type SuperSessionPromptRequest, +} from "@/lib/super-session-prompt"; +import { api } from "@/utils/api"; + +const PROFILE_PATH = "/dashboard/settings/profile"; + +export const SuperPasswordUnlockDialog = () => { + const utils = api.useUtils(); + const [request, setRequest] = useState( + null, + ); + const [password, setPassword] = useState(""); + const unlock = api.superPassword.unlock.useMutation(); + const { data: status } = api.superPassword.status.useQuery(undefined, { + enabled: !!request, + }); + + useEffect(() => registerSuperSessionPrompt(setRequest), []); + + const finish = (unlocked: boolean) => { + request?.resolve(unlocked); + setRequest(null); + setPassword(""); + unlock.reset(); + }; + + const handleUnlock = async (event: React.FormEvent) => { + event.preventDefault(); + try { + await unlock.mutateAsync({ password }); + await utils.superPassword.status.invalidate(); + toast.success("Super password access is open for 24 hours"); + finish(true); + } catch { + // The error is shown inside the dialog. + } + }; + + return ( + { + if (!open) { + finish(false); + } + }} + > + + + Super password required + + This action needs open super password access. Enter your super + password to open access for 24 hours;{" "} + {request?.retries + ? "the action is retried right after." + : "then run the action again."} + + + + {status && !status.isSet ? ( + + You have not set a super password yet. Set one in{" "} + finish(false)} + > + Profile → Super password + {" "} + to use this action. + + ) : ( +
+ {unlock.isError && ( + {unlock.error.message} + )} +
+ + setPassword(event.target.value)} + /> + {status?.hint && ( +

+ Hint: {status.hint} +

+ )} +
+ + + + +
+ )} + +

+ Manage access in{" "} + finish(false)} + > + Profile → Super password + + . +

+
+
+ ); +}; diff --git a/apps/dokploy/drizzle/0204_super_password.sql b/apps/dokploy/drizzle/0204_super_password.sql new file mode 100644 index 00000000000..e2b075a4bc2 --- /dev/null +++ b/apps/dokploy/drizzle/0204_super_password.sql @@ -0,0 +1,33 @@ +CREATE TABLE "super_password" ( + "user_id" text PRIMARY KEY NOT NULL, + "password_hash" text NOT NULL, + "hint" text, + "failed_attempts" integer DEFAULT 0 NOT NULL, + "locked_until" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "super_password_token" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "type" text NOT NULL, + "token_hash" text NOT NULL, + "expires_at" timestamp NOT NULL, + "used_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "super_password_token_token_hash_unique" UNIQUE("token_hash") +); +--> statement-breakpoint +CREATE TABLE "super_session" ( + "user_id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "opened_at" timestamp NOT NULL, + "opened_by_session_id" text, + "last_extended_at" timestamp +); +--> statement-breakpoint +ALTER TABLE "super_password" ADD CONSTRAINT "super_password_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "super_password_token" ADD CONSTRAINT "super_password_token_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "super_session" ADD CONSTRAINT "super_session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "super_password_token_user_id_idx" ON "super_password_token" USING btree ("user_id"); \ No newline at end of file diff --git a/apps/dokploy/drizzle/0205_super_password_alert_channels.sql b/apps/dokploy/drizzle/0205_super_password_alert_channels.sql new file mode 100644 index 00000000000..08636a6b022 --- /dev/null +++ b/apps/dokploy/drizzle/0205_super_password_alert_channels.sql @@ -0,0 +1,3 @@ +ALTER TABLE "notification" ADD COLUMN "superPassword" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "super_password" ADD COLUMN "reset_request_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "super_password" ADD COLUMN "reset_window_started_at" timestamp; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0204_snapshot.json b/apps/dokploy/drizzle/meta/0204_snapshot.json new file mode 100644 index 00000000000..a82c822f1a9 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0204_snapshot.json @@ -0,0 +1,10949 @@ +{ + "id": "adb31649-783f-4a68-bab1-0730cab17fd2", + "prevId": "200d7782-0a11-4a28-a54c-3acf023d9f22", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteEnvironments": { + "name": "canDeleteEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateEnvironments": { + "name": "canCreateEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedEnvironments": { + "name": "accessedEnvironments", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedGitProviders": { + "name": "accessedGitProviders", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedServers": { + "name": "accessedServers", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_role": { + "name": "default_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_id_fk": { + "name": "organization_owner_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_role": { + "name": "organization_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organizationRole_organizationId_idx": { + "name": "organizationRole_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizationRole_role_idx": { + "name": "organizationRole_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_role_organization_id_organization_id_fk": { + "name": "organization_role_organization_id_organization_id_fk", + "tableFrom": "organization_role", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildSecrets": { + "name": "previewBuildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLabels": { + "name": "previewLabels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewRequireCollaboratorPermissions": { + "name": "previewRequireCollaboratorPermissions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rollbackActive": { + "name": "rollbackActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildSecrets": { + "name": "buildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Dockerfile'" + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "railpackVersion": { + "name": "railpackVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0.15.4'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackRegistryId": { + "name": "rollbackRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildRegistryId": { + "name": "buildRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_rollbackRegistryId_registry_registryId_fk": { + "name": "application_rollbackRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "rollbackRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_environmentId_environment_environmentId_fk": { + "name": "application_environmentId_environment_environmentId_fk", + "tableFrom": "application", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_buildServerId_server_serverId_fk": { + "name": "application_buildServerId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_buildRegistryId_registry_registryId_fk": { + "name": "application_buildRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "buildRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_role": { + "name": "user_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auditLog_organizationId_idx": { + "name": "auditLog_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_userId_idx": { + "name": "auditLog_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_createdAt_idx": { + "name": "auditLog_createdAt_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_organization_id_organization_id_fk": { + "name": "audit_log_organization_id_organization_id_fk", + "tableFrom": "audit_log", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_user_id_user_id_fk": { + "name": "audit_log_user_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "includeEncryptionKey": { + "name": "includeEncryptionKey", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_libsqlId_libsql_libsqlId_fk": { + "name": "backup_libsqlId_libsql_libsqlId_fk", + "tableFrom": "backup", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_userId_user_id_fk": { + "name": "backup_userId_user_id_fk", + "tableFrom": "backup", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketEmail": { + "name": "bitbucketEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeploymentsVolume": { + "name": "isolatedDeploymentsVolume", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pullImages": { + "name": "pullImages", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceNetworks": { + "name": "serviceNetworks", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_environmentId_environment_environmentId_fk": { + "name": "compose_environmentId_environment_environmentId_fk", + "tableFrom": "compose", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deploymentOperation": { + "name": "deploymentOperation", + "schema": "", + "columns": { + "operationId": { + "name": "operationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotencyKeyHash": { + "name": "idempotencyKeyHash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sourceRevision": { + "name": "sourceRevision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolvedRevision": { + "name": "resolvedRevision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "envRevision": { + "name": "envRevision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "deploymentOperationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'accepted'" + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deploymentOperation_composeId_compose_composeId_fk": { + "name": "deploymentOperation_composeId_compose_composeId_fk", + "tableFrom": "deploymentOperation", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deploymentOperation_deploymentId_deployment_deploymentId_fk": { + "name": "deploymentOperation_deploymentId_deployment_deploymentId_fk", + "tableFrom": "deploymentOperation", + "tableTo": "deployment", + "columnsFrom": [ + "deploymentId" + ], + "columnsTo": [ + "deploymentId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_operation_compose_key_unique": { + "name": "deployment_operation_compose_key_unique", + "nullsNotDistinct": false, + "columns": [ + "composeId", + "idempotencyKeyHash" + ] + }, + "deployment_operation_deployment_unique": { + "name": "deployment_operation_deployment_unique", + "nullsNotDistinct": false, + "columns": [ + "deploymentId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "tableTo": "schedule", + "columnsFrom": [ + "scheduleId" + ], + "columnsTo": [ + "scheduleId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "tableTo": "backup", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "backupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "tableTo": "rollback", + "columnsFrom": [ + "rollbackId" + ], + "columnsTo": [ + "rollbackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": { + "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk", + "tableFrom": "deployment", + "tableTo": "volume_backup", + "columnsFrom": [ + "volumeBackupId" + ], + "columnsTo": [ + "volumeBackupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_buildServerId_server_serverId_fk": { + "name": "deployment_buildServerId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "additionalFlags": { + "name": "additionalFlags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dns_provider": { + "name": "dns_provider", + "schema": "", + "columns": { + "dnsProviderId": { + "name": "dnsProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "DnsProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dns_provider_org_name_idx": { + "name": "dns_provider_org_name_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dns_provider_organizationId_organization_id_fk": { + "name": "dns_provider_organizationId_organization_id_fk", + "tableFrom": "dns_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "customEntrypoint": { + "name": "customEntrypoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "internalPath": { + "name": "internalPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "stripPath": { + "name": "stripPath", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "middlewares": { + "name": "middlewares", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "forwardAuthEnabled": { + "name": "forwardAuthEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "environment_projectId_project_projectId_fk": { + "name": "environment_projectId_project_projectId_fk", + "tableFrom": "environment", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forward_auth_settings": { + "name": "forward_auth_settings", + "schema": "", + "columns": { + "forwardAuthSettingsId": { + "name": "forwardAuthSettingsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "authDomain": { + "name": "authDomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "baseDomain": { + "name": "baseDomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'letsencrypt'" + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "forward_auth_settings_providerId_sso_provider_provider_id_fk": { + "name": "forward_auth_settings_providerId_sso_provider_provider_id_fk", + "tableFrom": "forward_auth_settings", + "tableTo": "sso_provider", + "columnsFrom": [ + "providerId" + ], + "columnsTo": [ + "provider_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "forward_auth_settings_serverId_server_serverId_fk": { + "name": "forward_auth_settings_serverId_server_serverId_fk", + "tableFrom": "forward_auth_settings", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "forward_auth_settings_serverId_unique": { + "name": "forward_auth_settings_serverId_unique", + "nullsNotDistinct": false, + "columns": [ + "serverId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sharedWithOrganization": { + "name": "sharedWithOrganization", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_provider_userId_user_id_fk": { + "name": "git_provider_userId_user_id_fk", + "tableFrom": "git_provider", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "giteaInternalUrl": { + "name": "giteaInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubUrl": { + "name": "githubUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://github.com'" + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "gitlabInternalUrl": { + "name": "gitlabInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.libsql": { + "name": "libsql", + "schema": "", + "columns": { + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sqldNode": { + "name": "sqldNode", + "type": "sqldNode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "sqldPrimaryUrl": { + "name": "sqldPrimaryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableNamespaces": { + "name": "enableNamespaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalGRPCPort": { + "name": "externalGRPCPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalAdminPort": { + "name": "externalAdminPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "libsql_environmentId_environment_environmentId_fk": { + "name": "libsql_environmentId_environment_environmentId_fk", + "tableFrom": "libsql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "libsql_serverId_server_serverId_fk": { + "name": "libsql_serverId_server_serverId_fk", + "tableFrom": "libsql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "libsql_appName_unique": { + "name": "libsql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_environmentId_environment_environmentId_fk": { + "name": "mariadb_environmentId_environment_environmentId_fk", + "tableFrom": "mariadb", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'mongo:8'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_environmentId_environment_environmentId_fk": { + "name": "mongo_environmentId_environment_environmentId_fk", + "tableFrom": "mongo", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_libsqlId_libsql_libsqlId_fk": { + "name": "mount_libsqlId_libsql_libsqlId_fk", + "tableFrom": "mount", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_environmentId_environment_environmentId_fk": { + "name": "mysql_environmentId_environment_environmentId_fk", + "tableFrom": "mysql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network": { + "name": "network", + "schema": "", + "columns": { + "networkId": { + "name": "networkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerId": { + "name": "dockerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver": { + "name": "driver", + "type": "networkDriver", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bridge'" + }, + "internal": { + "name": "internal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attachable": { + "name": "attachable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableIPv4": { + "name": "enableIPv4", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enableIPv6": { + "name": "enableIPv6", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mtu": { + "name": "mtu", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ipam": { + "name": "ipam", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "network_organizationId_organization_id_fk": { + "name": "network_organizationId_organization_id_fk", + "tableFrom": "network", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "network_serverId_server_serverId_fk": { + "name": "network_serverId_server_serverId_fk", + "tableFrom": "network", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom": { + "name": "custom", + "schema": "", + "columns": { + "customId": { + "name": "customId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lark": { + "name": "lark", + "schema": "", + "columns": { + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mattermost": { + "name": "mattermost", + "schema": "", + "columns": { + "mattermostId": { + "name": "mattermostId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "volumeBackup": { + "name": "volumeBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployBackup": { + "name": "dokployBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mattermostId": { + "name": "mattermostId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customId": { + "name": "customId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_resendId_resend_resendId_fk": { + "name": "notification_resendId_resend_resendId_fk", + "tableFrom": "notification", + "tableTo": "resend", + "columnsFrom": [ + "resendId" + ], + "columnsTo": [ + "resendId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "tableTo": "gotify", + "columnsFrom": [ + "gotifyId" + ], + "columnsTo": [ + "gotifyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_ntfyId_ntfy_ntfyId_fk": { + "name": "notification_ntfyId_ntfy_ntfyId_fk", + "tableFrom": "notification", + "tableTo": "ntfy", + "columnsFrom": [ + "ntfyId" + ], + "columnsTo": [ + "ntfyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_mattermostId_mattermost_mattermostId_fk": { + "name": "notification_mattermostId_mattermost_mattermostId_fk", + "tableFrom": "notification", + "tableTo": "mattermost", + "columnsFrom": [ + "mattermostId" + ], + "columnsTo": [ + "mattermostId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_customId_custom_customId_fk": { + "name": "notification_customId_custom_customId_fk", + "tableFrom": "notification", + "tableTo": "custom", + "columnsFrom": [ + "customId" + ], + "columnsTo": [ + "customId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_larkId_lark_larkId_fk": { + "name": "notification_larkId_lark_larkId_fk", + "tableFrom": "notification", + "tableTo": "lark", + "columnsFrom": [ + "larkId" + ], + "columnsTo": [ + "larkId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_pushoverId_pushover_pushoverId_fk": { + "name": "notification_pushoverId_pushover_pushoverId_fk", + "tableFrom": "notification", + "tableTo": "pushover", + "columnsFrom": [ + "pushoverId" + ], + "columnsTo": [ + "pushoverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_teamsId_teams_teamsId_fk": { + "name": "notification_teamsId_teams_teamsId_fk", + "tableFrom": "notification", + "tableTo": "teams", + "columnsFrom": [ + "teamsId" + ], + "columnsTo": [ + "teamsId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ntfy": { + "name": "ntfy", + "schema": "", + "columns": { + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pushover": { + "name": "pushover", + "schema": "", + "columns": { + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userKey": { + "name": "userKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry": { + "name": "retry", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expire": { + "name": "expire", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resend": { + "name": "resend", + "schema": "", + "columns": { + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patch": { + "name": "patch", + "schema": "", + "columns": { + "patchId": { + "name": "patchId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "patchType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'update'" + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "patch_applicationId_application_applicationId_fk": { + "name": "patch_applicationId_application_applicationId_fk", + "tableFrom": "patch", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "patch_composeId_compose_composeId_fk": { + "name": "patch_composeId_compose_composeId_fk", + "tableFrom": "patch", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "patch_filepath_application_unique": { + "name": "patch_filepath_application_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "applicationId" + ] + }, + "patch_filepath_compose_unique": { + "name": "patch_filepath_compose_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "composeId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publishMode": { + "name": "publishMode", + "type": "publishModeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'host'" + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_environmentId_environment_environmentId_fk": { + "name": "postgres_environmentId_environment_environmentId_fk", + "tableFrom": "postgres", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_environmentId_environment_environmentId_fk": { + "name": "redis_environmentId_environment_environmentId_fk", + "tableFrom": "redis", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollback": { + "name": "rollback", + "schema": "", + "columns": { + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullContext": { + "name": "fullContext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", + "tableFrom": "rollback", + "tableTo": "deployment", + "columnsFrom": [ + "deploymentId" + ], + "columnsTo": [ + "deploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_organizationId_organization_id_fk": { + "name": "schedule_organizationId_organization_id_fk", + "tableFrom": "schedule", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection_binding": { + "name": "scim_connection_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_key": { + "name": "connection_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_status": { + "name": "decommission_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "decommission_cursor_user_id": { + "name": "decommission_cursor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_reconciled_user_count": { + "name": "decommission_reconciled_user_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_batch_count": { + "name": "decommission_batch_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_revision": { + "name": "decommission_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_completed_at": { + "name": "decommission_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_id": { + "name": "decommission_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_expires_at": { + "name": "decommission_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimConnectionBinding_connectionId_idx": { + "name": "scimConnectionBinding_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_connection_binding_connection_key_unique": { + "name": "scim_connection_binding_connection_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimGroup_connectionId_idx": { + "name": "scimGroup_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroup_provisioningDomainId_idx": { + "name": "scimGroup_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_display_name_key_unique": { + "name": "scim_group_display_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "display_name_key" + ] + }, + "scim_group_external_id_key_unique": { + "name": "scim_group_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_group_order_key_unique": { + "name": "scim_group_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_key": { + "name": "membership_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimGroupMember_connectionId_idx": { + "name": "scimGroupMember_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroupMember_groupId_idx": { + "name": "scimGroupMember_groupId_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroupMember_scimUserId_idx": { + "name": "scimGroupMember_scimUserId_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_member_membership_key_unique": { + "name": "scim_group_member_membership_key_unique", + "nullsNotDistinct": false, + "columns": [ + "membership_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_identity_tombstone": { + "name": "scim_identity_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile": { + "name": "profile", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimIdentityTombstone_connectionId_idx": { + "name": "scimIdentityTombstone_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimIdentityTombstone_provisioningDomainId_idx": { + "name": "scimIdentityTombstone_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimIdentityTombstone_userId_idx": { + "name": "scimIdentityTombstone_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_identity_tombstone_user_id_user_id_fk": { + "name": "scim_identity_tombstone_user_id_user_id_fk", + "tableFrom": "scim_identity_tombstone", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_identity_tombstone_external_id_key_unique": { + "name": "scim_identity_tombstone_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_legacy_identity": { + "name": "scim_legacy_identity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "legacy_provider_id": { + "name": "legacy_provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scim_legacy_identity_organization_id_organization_id_fk": { + "name": "scim_legacy_identity_organization_id_organization_id_fk", + "tableFrom": "scim_legacy_identity", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_legacy_identity_user_id_user_id_fk": { + "name": "scim_legacy_identity_user_id_user_id_fk", + "tableFrom": "scim_legacy_identity", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_legacy_identity_organization_external_key_unique": { + "name": "scim_legacy_identity_organization_external_key_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "external_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_connection": { + "name": "scim_managed_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "creation_request_id": { + "name": "creation_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decommission_started_at": { + "name": "decommission_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_started_by": { + "name": "decommission_started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommissioned_by": { + "name": "decommissioned_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimManagedConnection_provisioningDomainId_idx": { + "name": "scimManagedConnection_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_connection_creation_request_id_unique": { + "name": "scim_managed_connection_creation_request_id_unique", + "nullsNotDistinct": false, + "columns": [ + "creation_request_id" + ] + }, + "scim_managed_connection_connection_id_unique": { + "name": "scim_managed_connection_connection_id_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_connection_event": { + "name": "scim_managed_connection_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_record_id": { + "name": "connection_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimManagedConnectionEvent_connectionRecordId_idx": { + "name": "scimManagedConnectionEvent_connectionRecordId_idx", + "columns": [ + { + "expression": "connection_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk": { + "name": "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk", + "tableFrom": "scim_managed_connection_event", + "tableTo": "scim_managed_connection", + "columnsFrom": [ + "connection_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_connection_event_event_key_unique": { + "name": "scim_managed_connection_event_event_key_unique", + "nullsNotDistinct": false, + "columns": [ + "event_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_credential": { + "name": "scim_managed_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_record_id": { + "name": "connection_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_digest": { + "name": "token_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash_version": { + "name": "hash_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_slot_key": { + "name": "active_slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serialized_scopes": { + "name": "serialized_scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimManagedCredential_connectionRecordId_idx": { + "name": "scimManagedCredential_connectionRecordId_idx", + "columns": [ + { + "expression": "connection_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk": { + "name": "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk", + "tableFrom": "scim_managed_credential", + "tableTo": "scim_managed_connection", + "columnsFrom": [ + "connection_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_credential_credential_id_unique": { + "name": "scim_managed_credential_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + }, + "scim_managed_credential_active_slot_key_unique": { + "name": "scim_managed_credential_active_slot_key_unique", + "nullsNotDistinct": false, + "columns": [ + "active_slot_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_value": { + "name": "source_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_key": { + "name": "grant_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimProjectionGrant_connectionId_idx": { + "name": "scimProjectionGrant_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_provisioningDomainId_idx": { + "name": "scimProjectionGrant_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_scimUserId_idx": { + "name": "scimProjectionGrant_scimUserId_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_userId_idx": { + "name": "scimProjectionGrant_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_user_id_user_id_fk": { + "name": "scim_projection_grant_user_id_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_projection_grant_grant_key_unique": { + "name": "scim_projection_grant_grant_key_unique", + "nullsNotDistinct": false, + "columns": [ + "grant_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_subject": { + "name": "scim_subject", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_source_id": { + "name": "profile_source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimSubject_profileSourceId_idx": { + "name": "scimSubject_profileSourceId_idx", + "columns": [ + { + "expression": "profile_source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_subject_user_id_user_id_fk": { + "name": "scim_subject_user_id_user_id_fk", + "tableFrom": "scim_subject", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_subject_user_id_unique": { + "name": "scim_subject_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_user_key": { + "name": "connection_user_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name_key": { + "name": "user_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_email": { + "name": "primary_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "work_email_value_index": { + "name": "work_email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_value_index": { + "name": "email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "formatted_name": { + "name": "formatted_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serialized_emails": { + "name": "serialized_emails", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serialized_attributes": { + "name": "serialized_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimUser_connectionId_idx": { + "name": "scimUser_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimUser_provisioningDomainId_idx": { + "name": "scimUser_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimUser_userId_idx": { + "name": "scimUser_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_user_connection_user_key_unique": { + "name": "scim_user_connection_user_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_user_key" + ] + }, + "scim_user_user_name_key_unique": { + "name": "scim_user_user_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_name_key" + ] + }, + "scim_user_external_id_key_unique": { + "name": "scim_user_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_user_order_key_unique": { + "name": "scim_user_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "buildsConcurrency": { + "name": "buildsConcurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "serverType": { + "name": "serverType", + "type": "serverType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'deploy'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.super_password": { + "name": "super_password", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hint": { + "name": "hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failed_attempts": { + "name": "failed_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "super_password_user_id_user_id_fk": { + "name": "super_password_user_id_user_id_fk", + "tableFrom": "super_password", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.super_password_token": { + "name": "super_password_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "super_password_token_user_id_idx": { + "name": "super_password_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "super_password_token_user_id_user_id_fk": { + "name": "super_password_token_user_id_user_id_fk", + "tableFrom": "super_password_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "super_password_token_token_hash_unique": { + "name": "super_password_token_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.super_session": { + "name": "super_session", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "opened_by_session_id": { + "name": "opened_by_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_extended_at": { + "name": "last_extended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "super_session_user_id_user_id_fk": { + "name": "super_session_user_id_user_id_fk", + "tableFrom": "super_session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainVerified": { + "name": "domainVerified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_tag": { + "name": "project_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_tag_projectId_project_projectId_fk": { + "name": "project_tag_projectId_project_projectId_fk", + "tableFrom": "project_tag", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_tag_tagId_tag_tagId_fk": { + "name": "project_tag_tagId_tag_tagId_fk", + "tableFrom": "project_tag", + "tableTo": "tag", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "tagId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_project_tag": { + "name": "unique_project_tag", + "nullsNotDistinct": false, + "columns": [ + "projectId", + "tagId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "tag_organizationId_organization_id_fk": { + "name": "tag_organizationId_organization_id_fk", + "tableFrom": "tag", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_org_tag_name": { + "name": "unique_org_tag_name", + "nullsNotDistinct": false, + "columns": [ + "organizationId", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableEnterpriseFeatures": { + "name": "enableEnterpriseFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "licenseKey": { + "name": "licenseKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isValidEnterpriseLicense": { + "name": "isValidEnterpriseLicense", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sendInvoiceNotifications": { + "name": "sendInvoiceNotifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isEnterpriseCloud": { + "name": "isEnterpriseCloud", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trustedOrigins": { + "name": "trustedOrigins", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "bookmarkedTemplates": { + "name": "bookmarkedTemplates", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "onboardingCompletedAt": { + "name": "onboardingCompletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_provider": { + "name": "vault_provider", + "schema": "", + "columns": { + "vaultProviderId": { + "name": "vaultProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "VaultProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "assignments": { + "name": "assignments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vault_provider_org_name_idx": { + "name": "vault_provider_org_name_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_provider_organizationId_organization_id_fk": { + "name": "vault_provider_organizationId_organization_id_fk", + "tableFrom": "vault_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volume_backup": { + "name": "volume_backup", + "schema": "", + "columns": { + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnOff": { + "name": "turnOff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "volume_backup_applicationId_application_applicationId_fk": { + "name": "volume_backup_applicationId_application_applicationId_fk", + "tableFrom": "volume_backup", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_postgresId_postgres_postgresId_fk": { + "name": "volume_backup_postgresId_postgres_postgresId_fk", + "tableFrom": "volume_backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mariadbId_mariadb_mariadbId_fk": { + "name": "volume_backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "volume_backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mongoId_mongo_mongoId_fk": { + "name": "volume_backup_mongoId_mongo_mongoId_fk", + "tableFrom": "volume_backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mysqlId_mysql_mysqlId_fk": { + "name": "volume_backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_redisId_redis_redisId_fk": { + "name": "volume_backup_redisId_redis_redisId_fk", + "tableFrom": "volume_backup", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_libsqlId_libsql_libsqlId_fk": { + "name": "volume_backup_libsqlId_libsql_libsqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_composeId_compose_composeId_fk": { + "name": "volume_backup_composeId_compose_composeId_fk", + "tableFrom": "volume_backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_destinationId_destination_destinationId_fk": { + "name": "volume_backup_destinationId_destination_destinationId_fk", + "tableFrom": "volume_backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webServerSettings": { + "name": "webServerSettings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 0 * * *'" + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "whitelabelingConfig": { + "name": "whitelabelingConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"appName\":null,\"appDescription\":null,\"logoUrl\":null,\"faviconUrl\":null,\"customCss\":null,\"loginLogoUrl\":null,\"supportUrl\":null,\"docsUrl\":null,\"errorPageTitle\":null,\"errorPageDescription\":null,\"footerText\":null,\"ogImageUrl\":null}'::jsonb" + }, + "remoteServersOnly": { + "name": "remoteServersOnly", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "buildsConcurrency": { + "name": "buildsConcurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "enforceSSO": { + "name": "enforceSSO", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server", + "libsql" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.deploymentOperationStatus": { + "name": "deploymentOperationStatus", + "schema": "public", + "values": [ + "accepted", + "queued", + "dispatch_unknown", + "running", + "succeeded", + "failed" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error", + "cancelled" + ] + }, + "public.DnsProviderType": { + "name": "DnsProviderType", + "schema": "public", + "values": [ + "cloudflare", + "route53", + "porkbun", + "infomaniak", + "ovh" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose", + "libsql" + ] + }, + "public.networkDriver": { + "name": "networkDriver", + "schema": "public", + "values": [ + "bridge", + "overlay" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "resend", + "gotify", + "ntfy", + "mattermost", + "pushover", + "custom", + "lark", + "teams" + ] + }, + "public.patchType": { + "name": "patchType", + "schema": "public", + "values": [ + "create", + "update", + "delete" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.publishModeType": { + "name": "publishModeType", + "schema": "public", + "values": [ + "ingress", + "host" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.serverType": { + "name": "serverType", + "schema": "public", + "values": [ + "deploy", + "build" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.sqldNode": { + "name": "sqldNode", + "schema": "public", + "values": [ + "primary", + "replica" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + }, + "public.VaultProviderType": { + "name": "VaultProviderType", + "schema": "public", + "values": [ + "hashicorp", + "infisical", + "aws", + "aws-parameter-store", + "doppler", + "azure", + "scaleway", + "phase" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0205_snapshot.json b/apps/dokploy/drizzle/meta/0205_snapshot.json new file mode 100644 index 00000000000..6ea50fc461e --- /dev/null +++ b/apps/dokploy/drizzle/meta/0205_snapshot.json @@ -0,0 +1,10969 @@ +{ + "id": "73a79b82-265d-42f5-aa6b-32c372663807", + "prevId": "adb31649-783f-4a68-bab1-0730cab17fd2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteEnvironments": { + "name": "canDeleteEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateEnvironments": { + "name": "canCreateEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedEnvironments": { + "name": "accessedEnvironments", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedGitProviders": { + "name": "accessedGitProviders", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedServers": { + "name": "accessedServers", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_role": { + "name": "default_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_id_fk": { + "name": "organization_owner_id_user_id_fk", + "tableFrom": "organization", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_role": { + "name": "organization_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organizationRole_organizationId_idx": { + "name": "organizationRole_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizationRole_role_idx": { + "name": "organizationRole_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_role_organization_id_organization_id_fk": { + "name": "organization_role_organization_id_organization_id_fk", + "tableFrom": "organization_role", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildSecrets": { + "name": "previewBuildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLabels": { + "name": "previewLabels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewRequireCollaboratorPermissions": { + "name": "previewRequireCollaboratorPermissions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rollbackActive": { + "name": "rollbackActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildSecrets": { + "name": "buildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Dockerfile'" + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "railpackVersion": { + "name": "railpackVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0.15.4'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackRegistryId": { + "name": "rollbackRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildRegistryId": { + "name": "buildRegistryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_rollbackRegistryId_registry_registryId_fk": { + "name": "application_rollbackRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "rollbackRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_environmentId_environment_environmentId_fk": { + "name": "application_environmentId_environment_environmentId_fk", + "tableFrom": "application", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_buildServerId_server_serverId_fk": { + "name": "application_buildServerId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_buildRegistryId_registry_registryId_fk": { + "name": "application_buildRegistryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "buildRegistryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_role": { + "name": "user_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auditLog_organizationId_idx": { + "name": "auditLog_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_userId_idx": { + "name": "auditLog_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auditLog_createdAt_idx": { + "name": "auditLog_createdAt_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_organization_id_organization_id_fk": { + "name": "audit_log_organization_id_organization_id_fk", + "tableFrom": "audit_log", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_user_id_user_id_fk": { + "name": "audit_log_user_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "includeEncryptionKey": { + "name": "includeEncryptionKey", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_libsqlId_libsql_libsqlId_fk": { + "name": "backup_libsqlId_libsql_libsqlId_fk", + "tableFrom": "backup", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_userId_user_id_fk": { + "name": "backup_userId_user_id_fk", + "tableFrom": "backup", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketEmail": { + "name": "bitbucketEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepositorySlug": { + "name": "bitbucketRepositorySlug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createEnvFile": { + "name": "createEnvFile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeploymentsVolume": { + "name": "isolatedDeploymentsVolume", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pullImages": { + "name": "pullImages", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceNetworks": { + "name": "serviceNetworks", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_environmentId_environment_environmentId_fk": { + "name": "compose_environmentId_environment_environmentId_fk", + "tableFrom": "compose", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "tableTo": "gitea", + "columnsFrom": [ + "giteaId" + ], + "columnsTo": [ + "giteaId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deploymentOperation": { + "name": "deploymentOperation", + "schema": "", + "columns": { + "operationId": { + "name": "operationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotencyKeyHash": { + "name": "idempotencyKeyHash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sourceRevision": { + "name": "sourceRevision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolvedRevision": { + "name": "resolvedRevision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "envRevision": { + "name": "envRevision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "deploymentOperationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'accepted'" + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deploymentOperation_composeId_compose_composeId_fk": { + "name": "deploymentOperation_composeId_compose_composeId_fk", + "tableFrom": "deploymentOperation", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deploymentOperation_deploymentId_deployment_deploymentId_fk": { + "name": "deploymentOperation_deploymentId_deployment_deploymentId_fk", + "tableFrom": "deploymentOperation", + "tableTo": "deployment", + "columnsFrom": [ + "deploymentId" + ], + "columnsTo": [ + "deploymentId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_operation_compose_key_unique": { + "name": "deployment_operation_compose_key_unique", + "nullsNotDistinct": false, + "columns": [ + "composeId", + "idempotencyKeyHash" + ] + }, + "deployment_operation_deployment_unique": { + "name": "deployment_operation_deployment_unique", + "nullsNotDistinct": false, + "columns": [ + "deploymentId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildServerId": { + "name": "buildServerId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "tableTo": "schedule", + "columnsFrom": [ + "scheduleId" + ], + "columnsTo": [ + "scheduleId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "tableTo": "backup", + "columnsFrom": [ + "backupId" + ], + "columnsTo": [ + "backupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "tableTo": "rollback", + "columnsFrom": [ + "rollbackId" + ], + "columnsTo": [ + "rollbackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": { + "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk", + "tableFrom": "deployment", + "tableTo": "volume_backup", + "columnsFrom": [ + "volumeBackupId" + ], + "columnsTo": [ + "volumeBackupId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_buildServerId_server_serverId_fk": { + "name": "deployment_buildServerId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "buildServerId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "additionalFlags": { + "name": "additionalFlags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dns_provider": { + "name": "dns_provider", + "schema": "", + "columns": { + "dnsProviderId": { + "name": "dnsProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "DnsProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dns_provider_org_name_idx": { + "name": "dns_provider_org_name_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dns_provider_organizationId_organization_id_fk": { + "name": "dns_provider_organizationId_organization_id_fk", + "tableFrom": "dns_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "customEntrypoint": { + "name": "customEntrypoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "internalPath": { + "name": "internalPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "stripPath": { + "name": "stripPath", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "middlewares": { + "name": "middlewares", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "forwardAuthEnabled": { + "name": "forwardAuthEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isDefault": { + "name": "isDefault", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "environment_projectId_project_projectId_fk": { + "name": "environment_projectId_project_projectId_fk", + "tableFrom": "environment", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forward_auth_settings": { + "name": "forward_auth_settings", + "schema": "", + "columns": { + "forwardAuthSettingsId": { + "name": "forwardAuthSettingsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "authDomain": { + "name": "authDomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "baseDomain": { + "name": "baseDomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'letsencrypt'" + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "forward_auth_settings_providerId_sso_provider_provider_id_fk": { + "name": "forward_auth_settings_providerId_sso_provider_provider_id_fk", + "tableFrom": "forward_auth_settings", + "tableTo": "sso_provider", + "columnsFrom": [ + "providerId" + ], + "columnsTo": [ + "provider_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "forward_auth_settings_serverId_server_serverId_fk": { + "name": "forward_auth_settings_serverId_server_serverId_fk", + "tableFrom": "forward_auth_settings", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "forward_auth_settings_serverId_unique": { + "name": "forward_auth_settings_serverId_unique", + "nullsNotDistinct": false, + "columns": [ + "serverId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sharedWithOrganization": { + "name": "sharedWithOrganization", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_provider_userId_user_id_fk": { + "name": "git_provider_userId_user_id_fk", + "tableFrom": "git_provider", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "giteaInternalUrl": { + "name": "giteaInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubUrl": { + "name": "githubUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://github.com'" + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "gitlabInternalUrl": { + "name": "gitlabInternalUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.libsql": { + "name": "libsql", + "schema": "", + "columns": { + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sqldNode": { + "name": "sqldNode", + "type": "sqldNode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "sqldPrimaryUrl": { + "name": "sqldPrimaryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableNamespaces": { + "name": "enableNamespaces", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalGRPCPort": { + "name": "externalGRPCPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalAdminPort": { + "name": "externalAdminPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "libsql_environmentId_environment_environmentId_fk": { + "name": "libsql_environmentId_environment_environmentId_fk", + "tableFrom": "libsql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "libsql_serverId_server_serverId_fk": { + "name": "libsql_serverId_server_serverId_fk", + "tableFrom": "libsql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "libsql_appName_unique": { + "name": "libsql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_environmentId_environment_environmentId_fk": { + "name": "mariadb_environmentId_environment_environmentId_fk", + "tableFrom": "mariadb", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'mongo:8'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_environmentId_environment_environmentId_fk": { + "name": "mongo_environmentId_environment_environmentId_fk", + "tableFrom": "mongo", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_libsqlId_libsql_libsqlId_fk": { + "name": "mount_libsqlId_libsql_libsqlId_fk", + "tableFrom": "mount", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_environmentId_environment_environmentId_fk": { + "name": "mysql_environmentId_environment_environmentId_fk", + "tableFrom": "mysql", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.network": { + "name": "network", + "schema": "", + "columns": { + "networkId": { + "name": "networkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerId": { + "name": "dockerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver": { + "name": "driver", + "type": "networkDriver", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bridge'" + }, + "internal": { + "name": "internal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attachable": { + "name": "attachable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableIPv4": { + "name": "enableIPv4", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enableIPv6": { + "name": "enableIPv6", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mtu": { + "name": "mtu", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ipam": { + "name": "ipam", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "network_organizationId_organization_id_fk": { + "name": "network_organizationId_organization_id_fk", + "tableFrom": "network", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "network_serverId_server_serverId_fk": { + "name": "network_serverId_server_serverId_fk", + "tableFrom": "network", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom": { + "name": "custom", + "schema": "", + "columns": { + "customId": { + "name": "customId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lark": { + "name": "lark", + "schema": "", + "columns": { + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mattermost": { + "name": "mattermost", + "schema": "", + "columns": { + "mattermostId": { + "name": "mattermostId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "volumeBackup": { + "name": "volumeBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployBackup": { + "name": "dokployBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "superPassword": { + "name": "superPassword", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mattermostId": { + "name": "mattermostId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customId": { + "name": "customId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_resendId_resend_resendId_fk": { + "name": "notification_resendId_resend_resendId_fk", + "tableFrom": "notification", + "tableTo": "resend", + "columnsFrom": [ + "resendId" + ], + "columnsTo": [ + "resendId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "tableTo": "gotify", + "columnsFrom": [ + "gotifyId" + ], + "columnsTo": [ + "gotifyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_ntfyId_ntfy_ntfyId_fk": { + "name": "notification_ntfyId_ntfy_ntfyId_fk", + "tableFrom": "notification", + "tableTo": "ntfy", + "columnsFrom": [ + "ntfyId" + ], + "columnsTo": [ + "ntfyId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_mattermostId_mattermost_mattermostId_fk": { + "name": "notification_mattermostId_mattermost_mattermostId_fk", + "tableFrom": "notification", + "tableTo": "mattermost", + "columnsFrom": [ + "mattermostId" + ], + "columnsTo": [ + "mattermostId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_customId_custom_customId_fk": { + "name": "notification_customId_custom_customId_fk", + "tableFrom": "notification", + "tableTo": "custom", + "columnsFrom": [ + "customId" + ], + "columnsTo": [ + "customId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_larkId_lark_larkId_fk": { + "name": "notification_larkId_lark_larkId_fk", + "tableFrom": "notification", + "tableTo": "lark", + "columnsFrom": [ + "larkId" + ], + "columnsTo": [ + "larkId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_pushoverId_pushover_pushoverId_fk": { + "name": "notification_pushoverId_pushover_pushoverId_fk", + "tableFrom": "notification", + "tableTo": "pushover", + "columnsFrom": [ + "pushoverId" + ], + "columnsTo": [ + "pushoverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_teamsId_teams_teamsId_fk": { + "name": "notification_teamsId_teams_teamsId_fk", + "tableFrom": "notification", + "tableTo": "teams", + "columnsFrom": [ + "teamsId" + ], + "columnsTo": [ + "teamsId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ntfy": { + "name": "ntfy", + "schema": "", + "columns": { + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pushover": { + "name": "pushover", + "schema": "", + "columns": { + "pushoverId": { + "name": "pushoverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userKey": { + "name": "userKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry": { + "name": "retry", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expire": { + "name": "expire", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resend": { + "name": "resend", + "schema": "", + "columns": { + "resendId": { + "name": "resendId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "teamsId": { + "name": "teamsId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patch": { + "name": "patch", + "schema": "", + "columns": { + "patchId": { + "name": "patchId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "patchType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'update'" + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "patch_applicationId_application_applicationId_fk": { + "name": "patch_applicationId_application_applicationId_fk", + "tableFrom": "patch", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "patch_composeId_compose_composeId_fk": { + "name": "patch_composeId_compose_composeId_fk", + "tableFrom": "patch", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "patch_filepath_application_unique": { + "name": "patch_filepath_application_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "applicationId" + ] + }, + "patch_filepath_compose_unique": { + "name": "patch_filepath_compose_unique", + "nullsNotDistinct": false, + "columns": [ + "filePath", + "composeId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publishMode": { + "name": "publishMode", + "type": "publishModeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'host'" + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_environmentId_environment_environmentId_fk": { + "name": "postgres_environmentId_environment_environmentId_fk", + "tableFrom": "postgres", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "args": { + "name": "args", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "ulimitsSwarm": { + "name": "ulimitsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "networkIds": { + "name": "networkIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "detachDokployNetwork": { + "name": "detachDokployNetwork", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_environmentId_environment_environmentId_fk": { + "name": "redis_environmentId_environment_environmentId_fk", + "tableFrom": "redis", + "tableTo": "environment", + "columnsFrom": [ + "environmentId" + ], + "columnsTo": [ + "environmentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollback": { + "name": "rollback", + "schema": "", + "columns": { + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullContext": { + "name": "fullContext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", + "tableFrom": "rollback", + "tableTo": "deployment", + "columnsFrom": [ + "deploymentId" + ], + "columnsTo": [ + "deploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedule_organizationId_organization_id_fk": { + "name": "schedule_organizationId_organization_id_fk", + "tableFrom": "schedule", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection_binding": { + "name": "scim_connection_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_key": { + "name": "connection_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_status": { + "name": "decommission_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "decommission_cursor_user_id": { + "name": "decommission_cursor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_reconciled_user_count": { + "name": "decommission_reconciled_user_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_batch_count": { + "name": "decommission_batch_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_revision": { + "name": "decommission_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_completed_at": { + "name": "decommission_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_id": { + "name": "decommission_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_expires_at": { + "name": "decommission_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimConnectionBinding_connectionId_idx": { + "name": "scimConnectionBinding_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_connection_binding_connection_key_unique": { + "name": "scim_connection_binding_connection_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimGroup_connectionId_idx": { + "name": "scimGroup_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroup_provisioningDomainId_idx": { + "name": "scimGroup_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_display_name_key_unique": { + "name": "scim_group_display_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "display_name_key" + ] + }, + "scim_group_external_id_key_unique": { + "name": "scim_group_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_group_order_key_unique": { + "name": "scim_group_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_key": { + "name": "membership_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimGroupMember_connectionId_idx": { + "name": "scimGroupMember_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroupMember_groupId_idx": { + "name": "scimGroupMember_groupId_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroupMember_scimUserId_idx": { + "name": "scimGroupMember_scimUserId_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_member_membership_key_unique": { + "name": "scim_group_member_membership_key_unique", + "nullsNotDistinct": false, + "columns": [ + "membership_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_identity_tombstone": { + "name": "scim_identity_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile": { + "name": "profile", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimIdentityTombstone_connectionId_idx": { + "name": "scimIdentityTombstone_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimIdentityTombstone_provisioningDomainId_idx": { + "name": "scimIdentityTombstone_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimIdentityTombstone_userId_idx": { + "name": "scimIdentityTombstone_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_identity_tombstone_user_id_user_id_fk": { + "name": "scim_identity_tombstone_user_id_user_id_fk", + "tableFrom": "scim_identity_tombstone", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_identity_tombstone_external_id_key_unique": { + "name": "scim_identity_tombstone_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_legacy_identity": { + "name": "scim_legacy_identity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "legacy_provider_id": { + "name": "legacy_provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scim_legacy_identity_organization_id_organization_id_fk": { + "name": "scim_legacy_identity_organization_id_organization_id_fk", + "tableFrom": "scim_legacy_identity", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_legacy_identity_user_id_user_id_fk": { + "name": "scim_legacy_identity_user_id_user_id_fk", + "tableFrom": "scim_legacy_identity", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_legacy_identity_organization_external_key_unique": { + "name": "scim_legacy_identity_organization_external_key_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "external_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_connection": { + "name": "scim_managed_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "creation_request_id": { + "name": "creation_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decommission_started_at": { + "name": "decommission_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_started_by": { + "name": "decommission_started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommissioned_by": { + "name": "decommissioned_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimManagedConnection_provisioningDomainId_idx": { + "name": "scimManagedConnection_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_connection_creation_request_id_unique": { + "name": "scim_managed_connection_creation_request_id_unique", + "nullsNotDistinct": false, + "columns": [ + "creation_request_id" + ] + }, + "scim_managed_connection_connection_id_unique": { + "name": "scim_managed_connection_connection_id_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_connection_event": { + "name": "scim_managed_connection_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_record_id": { + "name": "connection_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimManagedConnectionEvent_connectionRecordId_idx": { + "name": "scimManagedConnectionEvent_connectionRecordId_idx", + "columns": [ + { + "expression": "connection_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk": { + "name": "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk", + "tableFrom": "scim_managed_connection_event", + "tableTo": "scim_managed_connection", + "columnsFrom": [ + "connection_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_connection_event_event_key_unique": { + "name": "scim_managed_connection_event_event_key_unique", + "nullsNotDistinct": false, + "columns": [ + "event_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_credential": { + "name": "scim_managed_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_record_id": { + "name": "connection_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_digest": { + "name": "token_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash_version": { + "name": "hash_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_slot_key": { + "name": "active_slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serialized_scopes": { + "name": "serialized_scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimManagedCredential_connectionRecordId_idx": { + "name": "scimManagedCredential_connectionRecordId_idx", + "columns": [ + { + "expression": "connection_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk": { + "name": "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk", + "tableFrom": "scim_managed_credential", + "tableTo": "scim_managed_connection", + "columnsFrom": [ + "connection_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_credential_credential_id_unique": { + "name": "scim_managed_credential_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + }, + "scim_managed_credential_active_slot_key_unique": { + "name": "scim_managed_credential_active_slot_key_unique", + "nullsNotDistinct": false, + "columns": [ + "active_slot_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_value": { + "name": "source_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_key": { + "name": "grant_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimProjectionGrant_connectionId_idx": { + "name": "scimProjectionGrant_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_provisioningDomainId_idx": { + "name": "scimProjectionGrant_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_scimUserId_idx": { + "name": "scimProjectionGrant_scimUserId_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_userId_idx": { + "name": "scimProjectionGrant_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_user_id_user_id_fk": { + "name": "scim_projection_grant_user_id_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_projection_grant_grant_key_unique": { + "name": "scim_projection_grant_grant_key_unique", + "nullsNotDistinct": false, + "columns": [ + "grant_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_subject": { + "name": "scim_subject", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_source_id": { + "name": "profile_source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimSubject_profileSourceId_idx": { + "name": "scimSubject_profileSourceId_idx", + "columns": [ + { + "expression": "profile_source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_subject_user_id_user_id_fk": { + "name": "scim_subject_user_id_user_id_fk", + "tableFrom": "scim_subject", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_subject_user_id_unique": { + "name": "scim_subject_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_user_key": { + "name": "connection_user_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name_key": { + "name": "user_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_email": { + "name": "primary_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "work_email_value_index": { + "name": "work_email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_value_index": { + "name": "email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "formatted_name": { + "name": "formatted_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serialized_emails": { + "name": "serialized_emails", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serialized_attributes": { + "name": "serialized_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimUser_connectionId_idx": { + "name": "scimUser_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimUser_provisioningDomainId_idx": { + "name": "scimUser_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimUser_userId_idx": { + "name": "scimUser_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_user_connection_user_key_unique": { + "name": "scim_user_connection_user_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_user_key" + ] + }, + "scim_user_user_name_key_unique": { + "name": "scim_user_user_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_name_key" + ] + }, + "scim_user_external_id_key_unique": { + "name": "scim_user_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_user_order_key_unique": { + "name": "scim_user_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "buildsConcurrency": { + "name": "buildsConcurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "serverType": { + "name": "serverType", + "type": "serverType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'deploy'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainVerified": { + "name": "domainVerified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.super_password": { + "name": "super_password", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hint": { + "name": "hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failed_attempts": { + "name": "failed_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reset_request_count": { + "name": "reset_request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_window_started_at": { + "name": "reset_window_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "super_password_user_id_user_id_fk": { + "name": "super_password_user_id_user_id_fk", + "tableFrom": "super_password", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.super_password_token": { + "name": "super_password_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "super_password_token_user_id_idx": { + "name": "super_password_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "super_password_token_user_id_user_id_fk": { + "name": "super_password_token_user_id_user_id_fk", + "tableFrom": "super_password_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "super_password_token_token_hash_unique": { + "name": "super_password_token_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.super_session": { + "name": "super_session", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "opened_by_session_id": { + "name": "opened_by_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_extended_at": { + "name": "last_extended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "super_session_user_id_user_id_fk": { + "name": "super_session_user_id_user_id_fk", + "tableFrom": "super_session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_tag": { + "name": "project_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "project_tag_projectId_project_projectId_fk": { + "name": "project_tag_projectId_project_projectId_fk", + "tableFrom": "project_tag", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_tag_tagId_tag_tagId_fk": { + "name": "project_tag_tagId_tag_tagId_fk", + "tableFrom": "project_tag", + "tableTo": "tag", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "tagId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_project_tag": { + "name": "unique_project_tag", + "nullsNotDistinct": false, + "columns": [ + "projectId", + "tagId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "tag_organizationId_organization_id_fk": { + "name": "tag_organizationId_organization_id_fk", + "tableFrom": "tag", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_org_tag_name": { + "name": "unique_org_tag_name", + "nullsNotDistinct": false, + "columns": [ + "organizationId", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableEnterpriseFeatures": { + "name": "enableEnterpriseFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "licenseKey": { + "name": "licenseKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isValidEnterpriseLicense": { + "name": "isValidEnterpriseLicense", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sendInvoiceNotifications": { + "name": "sendInvoiceNotifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isEnterpriseCloud": { + "name": "isEnterpriseCloud", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trustedOrigins": { + "name": "trustedOrigins", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "bookmarkedTemplates": { + "name": "bookmarkedTemplates", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "onboardingCompletedAt": { + "name": "onboardingCompletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_provider": { + "name": "vault_provider", + "schema": "", + "columns": { + "vaultProviderId": { + "name": "vaultProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "VaultProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "assignments": { + "name": "assignments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vault_provider_org_name_idx": { + "name": "vault_provider_org_name_idx", + "columns": [ + { + "expression": "organizationId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_provider_organizationId_organization_id_fk": { + "name": "vault_provider_organizationId_organization_id_fk", + "tableFrom": "vault_provider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volume_backup": { + "name": "volume_backup", + "schema": "", + "columns": { + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnOff": { + "name": "turnOff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "libsqlId": { + "name": "libsqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "volume_backup_applicationId_application_applicationId_fk": { + "name": "volume_backup_applicationId_application_applicationId_fk", + "tableFrom": "volume_backup", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_postgresId_postgres_postgresId_fk": { + "name": "volume_backup_postgresId_postgres_postgresId_fk", + "tableFrom": "volume_backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mariadbId_mariadb_mariadbId_fk": { + "name": "volume_backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "volume_backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mongoId_mongo_mongoId_fk": { + "name": "volume_backup_mongoId_mongo_mongoId_fk", + "tableFrom": "volume_backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_mysqlId_mysql_mysqlId_fk": { + "name": "volume_backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_redisId_redis_redisId_fk": { + "name": "volume_backup_redisId_redis_redisId_fk", + "tableFrom": "volume_backup", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_libsqlId_libsql_libsqlId_fk": { + "name": "volume_backup_libsqlId_libsql_libsqlId_fk", + "tableFrom": "volume_backup", + "tableTo": "libsql", + "columnsFrom": [ + "libsqlId" + ], + "columnsTo": [ + "libsqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_composeId_compose_composeId_fk": { + "name": "volume_backup_composeId_compose_composeId_fk", + "tableFrom": "volume_backup", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "volume_backup_destinationId_destination_destinationId_fk": { + "name": "volume_backup_destinationId_destination_destinationId_fk", + "tableFrom": "volume_backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webServerSettings": { + "name": "webServerSettings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 0 * * *'" + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "whitelabelingConfig": { + "name": "whitelabelingConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"appName\":null,\"appDescription\":null,\"logoUrl\":null,\"faviconUrl\":null,\"customCss\":null,\"loginLogoUrl\":null,\"supportUrl\":null,\"docsUrl\":null,\"errorPageTitle\":null,\"errorPageDescription\":null,\"footerText\":null,\"ogImageUrl\":null}'::jsonb" + }, + "remoteServersOnly": { + "name": "remoteServersOnly", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "buildsConcurrency": { + "name": "buildsConcurrency", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "enforceSSO": { + "name": "enforceSSO", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server", + "libsql" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.deploymentOperationStatus": { + "name": "deploymentOperationStatus", + "schema": "public", + "values": [ + "accepted", + "queued", + "dispatch_unknown", + "running", + "succeeded", + "failed" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error", + "cancelled" + ] + }, + "public.DnsProviderType": { + "name": "DnsProviderType", + "schema": "public", + "values": [ + "cloudflare", + "route53", + "porkbun", + "infomaniak", + "ovh" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose", + "libsql" + ] + }, + "public.networkDriver": { + "name": "networkDriver", + "schema": "public", + "values": [ + "bridge", + "overlay" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "resend", + "gotify", + "ntfy", + "mattermost", + "pushover", + "custom", + "lark", + "teams" + ] + }, + "public.patchType": { + "name": "patchType", + "schema": "public", + "values": [ + "create", + "update", + "delete" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.publishModeType": { + "name": "publishModeType", + "schema": "public", + "values": [ + "ingress", + "host" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.serverType": { + "name": "serverType", + "schema": "public", + "values": [ + "deploy", + "build" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.sqldNode": { + "name": "sqldNode", + "schema": "public", + "values": [ + "primary", + "replica" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + }, + "public.VaultProviderType": { + "name": "VaultProviderType", + "schema": "public", + "values": [ + "hashicorp", + "infisical", + "aws", + "aws-parameter-store", + "doppler", + "azure", + "scaleway", + "phase" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index fe1b958b35d..20ad987f700 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -1429,6 +1429,20 @@ "when": 1791296078177, "tag": "0203_repair_skipped_upstream_columns", "breakpoints": true + }, + { + "idx": 204, + "version": "7", + "when": 1791352716633, + "tag": "0204_super_password", + "breakpoints": true + }, + { + "idx": 205, + "version": "7", + "when": 1791357451882, + "tag": "0205_super_password_alert_channels", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/esbuild.config.ts b/apps/dokploy/esbuild.config.ts index f0d765f2610..211d8e2c813 100644 --- a/apps/dokploy/esbuild.config.ts +++ b/apps/dokploy/esbuild.config.ts @@ -28,6 +28,8 @@ try { "wait-for-postgres": "wait-for-postgres.ts", "reset-password": "reset-password.ts", "reset-2fa": "reset-2fa.ts", + "reset-super-password": "reset-super-password.ts", + "lock-super-session": "lock-super-session.ts", "migrate-auth-secret": "scripts/migrate-auth-secret.ts", }, bundle: true, diff --git a/apps/dokploy/lib/auth-client.ts b/apps/dokploy/lib/auth-client.ts index 31235c94ea3..496b7322505 100644 --- a/apps/dokploy/lib/auth-client.ts +++ b/apps/dokploy/lib/auth-client.ts @@ -8,9 +8,26 @@ import { twoFactorClient, } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; +import { requestSuperSessionUnlock } from "@/lib/super-session-prompt"; export const authClient = createAuthClient({ // baseURL: "http://localhost:3000", // the base url of your auth server + fetchOptions: { + // Auth endpoints that change account access answer with this code when + // the super session is closed; the caller still shows its own error. + onError: ({ error }) => { + if ( + (error as { code?: string } | undefined)?.code === + "SUPER_SESSION_REQUIRED" + ) { + void requestSuperSessionUnlock({ + reason: "super-session-required", + key: "auth", + retries: false, + }); + } + }, + }, plugins: [ organizationClient(), twoFactorClient(), diff --git a/apps/dokploy/lib/super-session-prompt.ts b/apps/dokploy/lib/super-session-prompt.ts new file mode 100644 index 00000000000..38eaa048305 --- /dev/null +++ b/apps/dokploy/lib/super-session-prompt.ts @@ -0,0 +1,60 @@ +import type { SuperSessionDenial } from "@dokploy/server/services/super-password"; + +export type SuperSessionPromptRequest = { + reason: SuperSessionDenial; + retries: boolean; + resolve: (unlocked: boolean) => void; +}; + +type Listener = (request: SuperSessionPromptRequest) => void; + +// React Query retries failed queries; without a cool-down a dismissed dialog +// would pop up again for each retry. +const DECLINE_COOL_DOWN_MS = 60_000; + +let listener: Listener | null = null; +let pending: Promise | null = null; +const declinedAt = new Map(); + +export const registerSuperSessionPrompt = (next: Listener) => { + listener = next; + return () => { + if (listener === next) { + listener = null; + } + }; +}; + +export const requestSuperSessionUnlock = ({ + reason, + key, + retries, +}: { + reason: SuperSessionDenial; + key: string; + retries: boolean; +}): Promise => { + const current = listener; + const lastDeclined = declinedAt.get(key); + if ( + !current || + (lastDeclined && Date.now() - lastDeclined < DECLINE_COOL_DOWN_MS) + ) { + return Promise.resolve(false); + } + if (!pending) { + pending = new Promise((resolve) => { + current({ reason, retries, resolve }); + }).finally(() => { + pending = null; + }); + } + return pending.then((unlocked) => { + if (unlocked) { + declinedAt.clear(); + } else { + declinedAt.set(key, Date.now()); + } + return unlocked; + }); +}; diff --git a/apps/dokploy/lock-super-session.ts b/apps/dokploy/lock-super-session.ts new file mode 100644 index 00000000000..43b8f5d73f6 --- /dev/null +++ b/apps/dokploy/lock-super-session.ts @@ -0,0 +1,37 @@ +import { findOwner } from "@dokploy/server"; +import { db } from "@dokploy/server/db"; +import { user } from "@dokploy/server/db/schema"; +import { lockSuperSessionOverSsh } from "@dokploy/server/services/super-password"; +import { eq } from "drizzle-orm"; + +(async () => { + try { + const email = process.argv[2]?.trim().toLowerCase(); + + let userId: string; + + if (email) { + const foundUser = await db.query.user.findFirst({ + where: eq(user.email, email), + }); + + if (!foundUser) { + console.log(`User not found for email: ${email}`); + process.exit(1); + } + + userId = foundUser.id; + } else { + const owner = await findOwner(); + userId = owner.userId; + } + + const closed = await lockSuperSessionOverSsh(userId); + + console.log(closed ? "Super session closed" : "No super session was open"); + process.exit(0); + } catch (error) { + console.log("Error closing the super session", error); + process.exit(1); + } +})(); diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 295ada0f0e1..77e197190fb 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -14,6 +14,8 @@ "wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts", "reset-password": "node -r dotenv/config dist/reset-password.mjs", "reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs", + "reset-super-password": "node -r dotenv/config dist/reset-super-password.mjs", + "lock-super-session": "node -r dotenv/config dist/lock-super-session.mjs", "migrate-auth-secret": "node -r dotenv/config dist/migrate-auth-secret.mjs", "dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ", "studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", diff --git a/apps/dokploy/pages/_app.tsx b/apps/dokploy/pages/_app.tsx index 1451162d52a..ed1f06ed35c 100644 --- a/apps/dokploy/pages/_app.tsx +++ b/apps/dokploy/pages/_app.tsx @@ -8,6 +8,7 @@ import NextTopLoader from "nextjs-toploader"; import type { ReactElement, ReactNode } from "react"; import { SearchCommand } from "@/components/dashboard/search-command"; import { Analytics } from "@/components/shared/analytics"; +import { SuperPasswordUnlockDialog } from "@/components/shared/super-password-unlock-dialog"; import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; @@ -50,6 +51,7 @@ const MyApp = ({ + {getLayout()} diff --git a/apps/dokploy/pages/api/application.env.upsert.ts b/apps/dokploy/pages/api/application.env.upsert.ts index ac9c7461b49..361913afb2d 100644 --- a/apps/dokploy/pages/api/application.env.upsert.ts +++ b/apps/dokploy/pages/api/application.env.upsert.ts @@ -6,10 +6,15 @@ import { } from "@dokploy/server"; import { apiUpsertApplicationEnv } from "@dokploy/server/db/schema"; import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; +import { + checkSuperSessionAccess, + SUPER_SESSION_MESSAGES, +} from "@dokploy/server/services/super-password"; import { TRPCError } from "@trpc/server"; import type { NextApiRequest, NextApiResponse } from "next"; import { ZodError } from "zod"; import { audit } from "@/server/api/utils/audit"; +import { isApiKeySession } from "@/server/api/utils/super-session"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; @@ -66,6 +71,18 @@ export const handleApplicationEnvUpsert = async ( return; } + const superSessionDenial = await checkSuperSessionAccess({ + userId: user.id, + kind: "write", + viaApiKey: isApiKeySession(session), + }); + if (superSessionDenial) { + res + .status(403) + .json({ message: SUPER_SESSION_MESSAGES[superSessionDenial] }); + return; + } + const ctx = { session: { ...session, diff --git a/apps/dokploy/pages/dashboard/settings/profile.tsx b/apps/dokploy/pages/dashboard/settings/profile.tsx index d04a2ede85e..d362d5cd939 100644 --- a/apps/dokploy/pages/dashboard/settings/profile.tsx +++ b/apps/dokploy/pages/dashboard/settings/profile.tsx @@ -7,6 +7,7 @@ import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys"; import { LinkingAccount } from "@/components/dashboard/settings/linking-account/linking-account"; import { DeleteAccount } from "@/components/dashboard/settings/profile/delete-account"; import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form"; +import { SuperPasswordCard } from "@/components/dashboard/settings/profile/super-password"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; @@ -19,6 +20,7 @@ const Page = () => {
+ {isCloud && } {permissions?.api.read && } diff --git a/apps/dokploy/pages/dashboard/settings/super-password-reset.tsx b/apps/dokploy/pages/dashboard/settings/super-password-reset.tsx new file mode 100644 index 00000000000..c3e896f6d6d --- /dev/null +++ b/apps/dokploy/pages/dashboard/settings/super-password-reset.tsx @@ -0,0 +1,144 @@ +import { validateRequest } from "@dokploy/server"; +import type { GetServerSidePropsContext } from "next"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { type ReactElement, useState } from "react"; +import { toast } from "sonner"; +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { api } from "@/utils/api"; + +const PROFILE_PATH = "/dashboard/settings/profile"; + +const Page = () => { + const router = useRouter(); + const token = + typeof router.query.token === "string" ? router.query.token : ""; + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [hint, setHint] = useState(""); + const reset = api.superPassword.resetWithToken.useMutation(); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (password !== confirm) { + toast.error("The passwords do not match"); + return; + } + try { + await reset.mutateAsync({ token, password, hint: hint || null }); + toast.success("Super password reset. Access is closed."); + await router.push(PROFILE_PATH); + } catch { + // The error is shown above the form. + } + }; + + return ( +
+ +
+ + Reset super password + + Set a new super password for the account you are signed in with. + The link works once and closes open super password access. + + + + {!token ? ( + + The reset link has no token. Request a new one in{" "} + + Profile → Super password + + . + + ) : ( +
+ {reset.isError && ( + {reset.error.message} + )} +
+ + setPassword(event.target.value)} + /> +
+
+ + setConfirm(event.target.value)} + /> +
+
+ + setHint(event.target.value)} + /> +
+
+ +
+
+ )} +
+
+
+
+ ); +}; + +export default Page; + +Page.getLayout = (page: ReactElement) => { + return ( + {page} + ); +}; + +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const { user } = await validateRequest(ctx.req); + if (!user) { + return { + redirect: { + permanent: false, + destination: "/", + }, + }; + } + return { props: {} }; +} diff --git a/apps/dokploy/pages/super-password/lock.tsx b/apps/dokploy/pages/super-password/lock.tsx new file mode 100644 index 00000000000..625cf60c193 --- /dev/null +++ b/apps/dokploy/pages/super-password/lock.tsx @@ -0,0 +1,69 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; +import type { ReactElement } from "react"; +import { OnboardingLayout } from "@/components/layouts/onboarding-layout"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { Logo } from "@/components/shared/logo"; +import { Button } from "@/components/ui/button"; +import { CardDescription, CardTitle } from "@/components/ui/card"; +import { api } from "@/utils/api"; + +// Opened from the "This wasn't me" link in the unlock email. It needs no +// sign-in and the token can only close access, so a button press (not the page +// load) does it: mail scanners that prefetch links must not close access. +export default function LockSuperSession() { + const router = useRouter(); + const token = + typeof router.query.token === "string" ? router.query.token : ""; + const lock = api.superPassword.lockWithToken.useMutation(); + + return ( +
+
+ + + + + Close super password access + + + Use this if you did not open super password access yourself. It closes + access right away; it can never open it. + + + {lock.data?.closed && ( + + Access is closed. Sign in, change your login password and super + password, and review your API keys and sessions. + + )} + {lock.data && !lock.data.closed && ( + + This link is invalid, expired or already used. If access is still + open, sign in and use Close access now in Profile, or run the + lock-super-session command over SSH. + + )} + {lock.isError && ( + {lock.error.message} + )} + + {!lock.data && ( + + )} +
+
+ ); +} + +LockSuperSession.getLayout = (page: ReactElement) => { + return {page}; +}; diff --git a/apps/dokploy/reset-super-password.ts b/apps/dokploy/reset-super-password.ts new file mode 100644 index 00000000000..535be9ec03c --- /dev/null +++ b/apps/dokploy/reset-super-password.ts @@ -0,0 +1,39 @@ +import { findOwner } from "@dokploy/server"; +import { db } from "@dokploy/server/db"; +import { user } from "@dokploy/server/db/schema"; +import { resetSuperPasswordOverSsh } from "@dokploy/server/services/super-password"; +import { eq } from "drizzle-orm"; + +(async () => { + try { + const email = process.argv[2]?.trim().toLowerCase(); + + let userId: string; + + if (email) { + const foundUser = await db.query.user.findFirst({ + where: eq(user.email, email), + }); + + if (!foundUser) { + console.log(`User not found for email: ${email}`); + process.exit(1); + } + + userId = foundUser.id; + } else { + const owner = await findOwner(); + userId = owner.userId; + } + + await resetSuperPasswordOverSsh(userId); + + console.log( + "Super password removed and super session closed. Set a new super password in Profile.", + ); + process.exit(0); + } catch (error) { + console.log("Error resetting the super password", error); + process.exit(1); + } +})(); diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index 64d61d3fbae..5c970f62824 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -51,6 +51,7 @@ import { serverRouter } from "./routers/server"; import { settingsRouter } from "./routers/settings"; import { sshRouter } from "./routers/ssh-key"; import { stripeRouter } from "./routers/stripe"; +import { superPasswordRouter } from "./routers/super-password"; import { swarmRouter } from "./routers/swarm"; import { tagRouter } from "./routers/tag"; import { transferRouter } from "./routers/transfer"; @@ -102,6 +103,7 @@ export const appRouter = createTRPCRouter({ settings: settingsRouter, sshKey: sshRouter, stripe: stripeRouter, + superPassword: superPasswordRouter, swarm: swarmRouter, user: userRouter, vaultProvider: vaultProviderRouter, diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 4db0c8e8f0a..cc5c03b90eb 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -1579,7 +1579,10 @@ export const applicationRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await checkServiceAccess(ctx, input.applicationId, "read"); + await checkServicePermissionAndAccess(ctx, input.applicationId, { + service: ["read"], + logs: ["read"], + }); const application = await findApplicationById(input.applicationId); if ( application.environment.project.organizationId !== diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index cce88ddda5a..39d56f6ca38 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -1568,7 +1568,10 @@ export const composeRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await checkServiceAccess(ctx, input.composeId, "read"); + await checkServicePermissionAndAccess(ctx, input.composeId, { + service: ["read"], + logs: ["read"], + }); const compose = await findComposeById(input.composeId); if ( compose.environment.project.organizationId !== diff --git a/apps/dokploy/server/api/routers/docker-volume.ts b/apps/dokploy/server/api/routers/docker-volume.ts index 643dff5d411..6dd94080ac2 100644 --- a/apps/dokploy/server/api/routers/docker-volume.ts +++ b/apps/dokploy/server/api/routers/docker-volume.ts @@ -12,7 +12,10 @@ import { import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { audit } from "@/server/api/utils/audit"; +import { isLocalSystemDockerVolume } from "@/server/api/utils/local-docker-access"; +import { assertProtectedResourceAccess } from "@/server/api/utils/super-session"; import { createTRPCRouter, withPermission } from "../trpc"; +import { assertDockerServerAccess } from "./docker"; export const volumeNameRegex = /^[a-zA-Z0-9.\-_]+$/; @@ -25,6 +28,19 @@ const volumePathSchema = z "Path must be absolute.", ); +const assertVolumeFileAccess = async ( + ctx: Parameters[0] & + Parameters[0], + volumeName: string, + serverId?: string, +) => { + await assertDockerServerAccess(ctx, serverId); + // Volumes of the panel's own containers hold its database and keys. + if (!serverId && (await isLocalSystemDockerVolume(volumeName))) { + await assertProtectedResourceAccess(ctx); + } +}; + export const dockerVolumeRouter = createTRPCRouter({ getVolumes: withPermission("docker", "read") .input( @@ -70,12 +86,7 @@ export const dockerVolumeRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + await assertVolumeFileAccess(ctx, input.volumeName, input.serverId); return await listVolumeFiles( input.volumeName, input.path, @@ -95,16 +106,11 @@ export const dockerVolumeRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + await assertVolumeFileAccess(ctx, input.volumeName, input.serverId); return await readVolumeFile(input.volumeName, input.path, input.serverId); }), - writeVolumeFile: withPermission("docker", "read") + writeVolumeFile: withPermission("docker", "write") .input( z.object({ volumeName: z @@ -117,12 +123,7 @@ export const dockerVolumeRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + await assertVolumeFileAccess(ctx, input.volumeName, input.serverId); await writeVolumeFile( input.volumeName, input.path, @@ -137,7 +138,7 @@ export const dockerVolumeRouter = createTRPCRouter({ }); }), - deleteVolumeFile: withPermission("docker", "read") + deleteVolumeFile: withPermission("docker", "delete") .input( z.object({ volumeName: z @@ -152,12 +153,7 @@ export const dockerVolumeRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + await assertVolumeFileAccess(ctx, input.volumeName, input.serverId); await deleteVolumeFile(input.volumeName, input.path, input.serverId); await audit(ctx, { action: "delete", @@ -187,7 +183,7 @@ export const dockerVolumeRouter = createTRPCRouter({ return await getVolumeConfig(input.volumeName, input.serverId); }), - removeVolume: withPermission("docker", "read") + removeVolume: withPermission("docker", "delete") .input( z.object({ volumeName: z @@ -198,12 +194,7 @@ export const dockerVolumeRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + await assertDockerServerAccess(ctx, input.serverId); await removeVolume(input.volumeName, input.serverId); await audit(ctx, { action: "delete", diff --git a/apps/dokploy/server/api/routers/docker.ts b/apps/dokploy/server/api/routers/docker.ts index 870a819085f..98c4e1929d1 100644 --- a/apps/dokploy/server/api/routers/docker.ts +++ b/apps/dokploy/server/api/routers/docker.ts @@ -29,14 +29,16 @@ import { z } from "zod"; import { audit } from "@/server/api/utils/audit"; import { assertLocalDockerContainerAccess, + assertLocalDockerContainerOrSystemAccess, type LocalDockerPermission, } from "@/server/api/utils/local-docker-access"; +import { assertProtectedResourceAccess } from "@/server/api/utils/super-session"; import { uploadFileToContainerSchema } from "@/utils/schema"; import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc"; export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/; -const assertDockerServerAccess = async ( +export const assertDockerServerAccess = async ( ctx: { session: { userId: string; @@ -95,6 +97,28 @@ const resolveAuthorizedContainerId = async ( return config?.Id || containerId; }; +// The file manager may also open the panel's own containers (dokploy, its +// postgres and redis, traefik), but only with an open super session. +const resolveAuthorizedFileContainerId = async ( + ctx: Parameters[0], + containerId: string, + serverId: string | undefined, + permission: LocalDockerPermission, +) => { + if (serverId) { + return containerId; + } + + const config = await assertLocalDockerContainerOrSystemAccess( + ctx, + containerId, + permission, + () => assertProtectedResourceAccess(ctx), + ); + + return config?.Id || containerId; +}; + const containerPathSchema = z .string() .min(1) @@ -400,17 +424,14 @@ export const dockerRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - return await listContainerFiles( + await assertDockerServerAccess(ctx, input.serverId); + const containerId = await resolveAuthorizedFileContainerId( + ctx, input.containerId, - input.path, input.serverId, + "read", ); + return await listContainerFiles(containerId, input.path, input.serverId); }), readContainerFile: withPermission("docker", "read") @@ -425,20 +446,17 @@ export const dockerRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - return await readContainerFile( + await assertDockerServerAccess(ctx, input.serverId); + const containerId = await resolveAuthorizedFileContainerId( + ctx, input.containerId, - input.path, input.serverId, + "read", ); + return await readContainerFile(containerId, input.path, input.serverId); }), - writeContainerFile: withPermission("docker", "read") + writeContainerFile: withPermission("docker", "write") .input( z.object({ containerId: z @@ -451,14 +469,15 @@ export const dockerRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - await writeContainerFile( + await assertDockerServerAccess(ctx, input.serverId); + const containerId = await resolveAuthorizedFileContainerId( + ctx, input.containerId, + input.serverId, + "write", + ); + await writeContainerFile( + containerId, input.path, input.content, input.serverId, @@ -466,12 +485,12 @@ export const dockerRouter = createTRPCRouter({ await audit(ctx, { action: "update", resourceType: "docker", - resourceId: input.containerId, - resourceName: `${input.containerId}:${input.path}`, + resourceId: containerId, + resourceName: `${containerId}:${input.path}`, }); }), - deleteContainerFile: withPermission("docker", "read") + deleteContainerFile: withPermission("docker", "delete") .input( z.object({ containerId: z @@ -486,18 +505,19 @@ export const dockerRouter = createTRPCRouter({ }), ) .mutation(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } - await deleteContainerFile(input.containerId, input.path, input.serverId); + await assertDockerServerAccess(ctx, input.serverId); + const containerId = await resolveAuthorizedFileContainerId( + ctx, + input.containerId, + input.serverId, + "delete", + ); + await deleteContainerFile(containerId, input.path, input.serverId); await audit(ctx, { action: "delete", resourceType: "docker", - resourceId: input.containerId, - resourceName: `${input.containerId}:${input.path}`, + resourceId: containerId, + resourceName: `${containerId}:${input.path}`, }); }), @@ -509,12 +529,7 @@ export const dockerRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - if (input.serverId) { - const server = await findServerById(input.serverId); - if (server.organizationId !== ctx.session?.activeOrganizationId) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - } + await assertDockerServerAccess(ctx, input.serverId); return await getDockerEvents(input.serverId, input.minutes); }), }); diff --git a/apps/dokploy/server/api/routers/libsql.ts b/apps/dokploy/server/api/routers/libsql.ts index 1b41b017bd3..1c54d717a96 100644 --- a/apps/dokploy/server/api/routers/libsql.ts +++ b/apps/dokploy/server/api/routers/libsql.ts @@ -531,7 +531,10 @@ export const libsqlRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await checkServiceAccess(ctx, input.libsqlId, "read"); + await checkServicePermissionAndAccess(ctx, input.libsqlId, { + service: ["read"], + logs: ["read"], + }); const libsql = await findLibsqlById(input.libsqlId); if ( libsql.environment.project.organizationId !== diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index ada5a8008d5..907946d25d3 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -673,7 +673,10 @@ export const mariadbRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await checkServiceAccess(ctx, input.mariadbId, "read"); + await checkServicePermissionAndAccess(ctx, input.mariadbId, { + service: ["read"], + logs: ["read"], + }); const mariadb = await findMariadbById(input.mariadbId); if ( mariadb.environment.project.organizationId !== diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index 87f821127ef..4d516cb4ae9 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -673,7 +673,10 @@ export const mongoRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await checkServiceAccess(ctx, input.mongoId, "read"); + await checkServicePermissionAndAccess(ctx, input.mongoId, { + service: ["read"], + logs: ["read"], + }); const mongo = await findMongoById(input.mongoId); if ( mongo.environment.project.organizationId !== diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index df2c93d6d7d..5af046aab90 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -687,7 +687,10 @@ export const mysqlRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await checkServiceAccess(ctx, input.mysqlId, "read"); + await checkServicePermissionAndAccess(ctx, input.mysqlId, { + service: ["read"], + logs: ["read"], + }); const mysql = await findMySqlById(input.mysqlId); if ( mysql.environment.project.organizationId !== diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index a335b18a8b0..269b9a076f4 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -41,6 +41,10 @@ import { updateTelegramNotification, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; +import { + findSuperPasswordChannelsInOrganization, + notifySuperPasswordChannelChanges, +} from "@dokploy/server/services/super-password"; import { redactNotificationSecrets, redactNotificationSecretsList, @@ -54,6 +58,7 @@ import { withPermission, } from "@/server/api/trpc"; import { audit } from "@/server/api/utils/audit"; +import { getRequestMeta } from "@/server/api/utils/request-meta"; import { apiCreateCustom, apiCreateDiscord, @@ -108,8 +113,34 @@ const assertNotificationProviderId = ( } }; +// Super password alert channels are recovery and alert paths, so every +// change to which channels have the toggle on (or where they point) is +// announced to all of them, old destinations included. +const watchSuperPasswordChannels = (action: "create" | "update" | "delete") => + withPermission("notification", action).use(async ({ ctx, next }) => { + const organizationId = ctx.session.activeOrganizationId; + const before = await findSuperPasswordChannelsInOrganization( + organizationId, + ).catch(() => null); + const result = await next(); + if (result.ok && before) { + void findSuperPasswordChannelsInOrganization(organizationId) + .then((after) => + notifySuperPasswordChannelChanges({ + before, + after, + context: { user: ctx.user, ...getRequestMeta(ctx.req) }, + }), + ) + .catch((error) => + console.error("Failed to check super password channels", error), + ); + } + return result; + }); + export const notificationRouter = createTRPCRouter({ - createSlack: withPermission("notification", "create") + createSlack: watchSuperPasswordChannels("create") .input(apiCreateSlack) .mutation(async ({ input, ctx }) => { try { @@ -128,7 +159,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateSlack: withPermission("notification", "update") + updateSlack: watchSuperPasswordChannels("update") .input(apiUpdateSlack) .mutation(async ({ input, ctx }) => { try { @@ -172,7 +203,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createTelegram: withPermission("notification", "create") + createTelegram: watchSuperPasswordChannels("create") .input(apiCreateTelegram) .mutation(async ({ input, ctx }) => { try { @@ -194,7 +225,7 @@ export const notificationRouter = createTRPCRouter({ } }), - updateTelegram: withPermission("notification", "update") + updateTelegram: watchSuperPasswordChannels("update") .input(apiUpdateTelegram) .mutation(async ({ input, ctx }) => { try { @@ -239,7 +270,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createDiscord: withPermission("notification", "create") + createDiscord: watchSuperPasswordChannels("create") .input(apiCreateDiscord) .mutation(async ({ input, ctx }) => { try { @@ -261,7 +292,7 @@ export const notificationRouter = createTRPCRouter({ } }), - updateDiscord: withPermission("notification", "update") + updateDiscord: watchSuperPasswordChannels("update") .input(apiUpdateDiscord) .mutation(async ({ input, ctx }) => { try { @@ -315,7 +346,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createEmail: withPermission("notification", "create") + createEmail: watchSuperPasswordChannels("create") .input(apiCreateEmail) .mutation(async ({ input, ctx }) => { try { @@ -333,7 +364,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateEmail: withPermission("notification", "update") + updateEmail: watchSuperPasswordChannels("update") .input(apiUpdateEmail) .mutation(async ({ input, ctx }) => { try { @@ -385,7 +416,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createResend: withPermission("notification", "create") + createResend: watchSuperPasswordChannels("create") .input(apiCreateResend) .mutation(async ({ input, ctx }) => { try { @@ -403,7 +434,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateResend: withPermission("notification", "update") + updateResend: watchSuperPasswordChannels("update") .input(apiUpdateResend) .mutation(async ({ input, ctx }) => { try { @@ -452,7 +483,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - remove: withPermission("notification", "delete") + remove: watchSuperPasswordChannels("delete") .input(apiFindOneNotification) .mutation(async ({ input, ctx }) => { try { @@ -582,7 +613,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createGotify: withPermission("notification", "create") + createGotify: watchSuperPasswordChannels("create") .input(apiCreateGotify) .mutation(async ({ input, ctx }) => { try { @@ -600,7 +631,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateGotify: withPermission("notification", "update") + updateGotify: watchSuperPasswordChannels("update") .input(apiUpdateGotify) .mutation(async ({ input, ctx }) => { try { @@ -645,7 +676,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createNtfy: withPermission("notification", "create") + createNtfy: watchSuperPasswordChannels("create") .input(apiCreateNtfy) .mutation(async ({ input, ctx }) => { try { @@ -663,7 +694,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateNtfy: withPermission("notification", "update") + updateNtfy: watchSuperPasswordChannels("update") .input(apiUpdateNtfy) .mutation(async ({ input, ctx }) => { try { @@ -713,7 +744,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createMattermost: withPermission("notification", "create") + createMattermost: watchSuperPasswordChannels("create") .input(apiCreateMattermost) .mutation(async ({ input, ctx }) => { try { @@ -734,7 +765,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateMattermost: withPermission("notification", "update") + updateMattermost: watchSuperPasswordChannels("update") .input(apiUpdateMattermost) .mutation(async ({ input, ctx }) => { try { @@ -782,7 +813,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createCustom: withPermission("notification", "create") + createCustom: watchSuperPasswordChannels("create") .input(apiCreateCustom) .mutation(async ({ input, ctx }) => { try { @@ -800,7 +831,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateCustom: withPermission("notification", "update") + updateCustom: watchSuperPasswordChannels("update") .input(apiUpdateCustom) .mutation(async ({ input, ctx }) => { try { @@ -845,7 +876,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createLark: withPermission("notification", "create") + createLark: watchSuperPasswordChannels("create") .input(apiCreateLark) .mutation(async ({ input, ctx }) => { try { @@ -863,7 +894,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateLark: withPermission("notification", "update") + updateLark: watchSuperPasswordChannels("update") .input(apiUpdateLark) .mutation(async ({ input, ctx }) => { try { @@ -909,7 +940,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createTeams: withPermission("notification", "create") + createTeams: watchSuperPasswordChannels("create") .input(apiCreateTeams) .mutation(async ({ input, ctx }) => { try { @@ -927,7 +958,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateTeams: withPermission("notification", "update") + updateTeams: watchSuperPasswordChannels("update") .input(apiUpdateTeams) .mutation(async ({ input, ctx }) => { try { @@ -971,7 +1002,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createPushover: withPermission("notification", "create") + createPushover: watchSuperPasswordChannels("create") .input(apiCreatePushover) .mutation(async ({ input, ctx }) => { try { @@ -992,7 +1023,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updatePushover: withPermission("notification", "update") + updatePushover: watchSuperPasswordChannels("update") .input(apiUpdatePushover) .mutation(async ({ input, ctx }) => { try { diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 31bc6549245..0e67575114c 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -685,7 +685,10 @@ export const postgresRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await checkServiceAccess(ctx, input.postgresId, "read"); + await checkServicePermissionAndAccess(ctx, input.postgresId, { + service: ["read"], + logs: ["read"], + }); const postgres = await findPostgresById(input.postgresId); if ( postgres.environment.project.organizationId !== diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index 99059bbf5cf..a1823cadc7c 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -658,7 +658,10 @@ export const redisRouter = createTRPCRouter({ }), ) .query(async ({ input, ctx }) => { - await checkServiceAccess(ctx, input.redisId, "read"); + await checkServicePermissionAndAccess(ctx, input.redisId, { + service: ["read"], + logs: ["read"], + }); const redis = await findRedisById(input.redisId); if ( redis.environment.project.organizationId !== diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 9ecdb933a48..984e9318f5a 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -12,6 +12,7 @@ import { cleanupSystem, cleanupVolumes, DEFAULT_UPDATE_DATA, + filterProtectedTraefikEntries, findServerById, getAccessibleServerIds, getAgentHitsUpdateCommand, @@ -53,6 +54,7 @@ import { } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { checkPermission } from "@dokploy/server/services/permission"; +import { isProtectedTraefikPath } from "@dokploy/server/utils/traefik/application"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; import { TRPCError } from "@trpc/server"; import { eq, sql } from "drizzle-orm"; @@ -60,6 +62,11 @@ import { scheduledJobs, scheduleJob } from "node-schedule"; import { parse, stringify } from "yaml"; import { z } from "zod"; import { audit } from "@/server/api/utils/audit"; +import { assertLocalHostAccess } from "@/server/api/utils/local-host-access"; +import { + assertProtectedResourceAccess, + hasProtectedResourceAccess, +} from "@/server/api/utils/super-session"; import { apiAssignDomain, apiEnableDashboard, @@ -108,6 +115,24 @@ const assertSettingsServerAccess = async ( } }; +const assertTraefikFilesHostAccess = async ( + ctx: { + user: { id: string }; + session: { + userId: string; + activeOrganizationId: string; + }; + }, + serverId?: string, +) => { + if (!serverId) { + await assertLocalHostAccess(ctx); + return; + } + + await assertSettingsServerAccess(ctx, serverId); +}; + export const settingsRouter = createTRPCRouter({ getWebServerSettings: protectedProcedure.query(async () => { if (IS_CLOUD) { @@ -657,10 +682,13 @@ export const settingsRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { try { await checkPermission(ctx, { traefikFiles: ["read"] }); - await assertSettingsServerAccess(ctx, input?.serverId); + await assertTraefikFilesHostAccess(ctx, input?.serverId); const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId); const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId); - return result || []; + if (await hasProtectedResourceAccess(ctx)) { + return result || []; + } + return filterProtectedTraefikEntries(result || [], input?.serverId); } catch (error) { throw error; } @@ -670,11 +698,19 @@ export const settingsRouter = createTRPCRouter({ .input(apiModifyTraefikConfig) .mutation(async ({ input, ctx }) => { await checkPermission(ctx, { traefikFiles: ["write"] }); - await assertSettingsServerAccess(ctx, input?.serverId); + await assertTraefikFilesHostAccess(ctx, input?.serverId); + const allowProtected = isProtectedTraefikPath( + input.path, + input?.serverId, + ); + if (allowProtected) { + await assertProtectedResourceAccess(ctx); + } await writeTraefikConfigInPath( input.path, input.traefikConfig, input?.serverId, + { allowProtected }, ); await audit(ctx, { action: "update", @@ -688,9 +724,13 @@ export const settingsRouter = createTRPCRouter({ .input(apiReadTraefikConfig) .query(async ({ input, ctx }) => { await checkPermission(ctx, { traefikFiles: ["read"] }); - await assertSettingsServerAccess(ctx, input.serverId); + await assertTraefikFilesHostAccess(ctx, input.serverId); + const allowProtected = isProtectedTraefikPath(input.path, input.serverId); + if (allowProtected) { + await assertProtectedResourceAccess(ctx); + } - return readConfigInPath(input.path, input.serverId); + return readConfigInPath(input.path, input.serverId, { allowProtected }); }), getIp: protectedProcedure.query(async () => { if (IS_CLOUD) { diff --git a/apps/dokploy/server/api/routers/super-password.ts b/apps/dokploy/server/api/routers/super-password.ts new file mode 100644 index 00000000000..a6a0806281f --- /dev/null +++ b/apps/dokploy/server/api/routers/super-password.ts @@ -0,0 +1,209 @@ +import { + changeSuperPassword, + closeSuperSession, + closeSuperSessionWithToken, + extendSuperSession, + getSuperSessionState, + listSuperPasswordRecoveryChannels, + openSuperSession, + removeSuperPassword, + requestSuperPasswordReset, + resetSuperPasswordWithToken, + SUPER_PASSWORD_HINT_MAX_LENGTH, + SUPER_SESSION_DURATION_MS, + sendSuperPasswordAlert, + setSuperPassword, + verifySuperPassword, +} from "@dokploy/server/services/super-password"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { audit } from "@/server/api/utils/audit"; +import { getRequestMeta } from "@/server/api/utils/request-meta"; +import { + assertBrowserSession, + isApiKeySession, +} from "@/server/api/utils/super-session"; +import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; + +const passwordSchema = z.string().min(1).max(256); +const hintSchema = z + .string() + .max(SUPER_PASSWORD_HINT_MAX_LENGTH) + .nullable() + .optional(); +const tokenSchema = z.string().min(1).max(256); + +const assertCanManageSuperPassword = (ctx: { user: { role: string } }) => { + if (ctx.user.role !== "owner" && ctx.user.role !== "admin") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Only owners and admins can use a super password.", + }); + } +}; + +const auditSuperPassword = ( + ctx: Parameters[0], + action: "create" | "update" | "delete" | "start" | "stop", + event: string, +) => + audit(ctx, { + action, + resourceType: "user", + resourceId: ctx.user.id, + resourceName: `super-password:${event}`, + }); + +const alertContext = (ctx: { + user: { id: string; email: string }; + req?: Parameters[0]; +}) => ({ user: ctx.user, ...getRequestMeta(ctx.req) }); + +export const superPasswordRouter = createTRPCRouter({ + status: protectedProcedure.query(async ({ ctx }) => { + const state = await getSuperSessionState(ctx.user.id); + return { + isSet: state.isSet, + hint: state.hint, + active: state.active, + expiresAt: state.expiresAt, + lockedUntil: state.lockedUntil, + durationMs: SUPER_SESSION_DURATION_MS, + canManage: ctx.user.role === "owner" || ctx.user.role === "admin", + viaApiKey: isApiKeySession(ctx.session), + recoveryChannels: await listSuperPasswordRecoveryChannels(ctx.user.id), + }; + }), + + set: protectedProcedure + .input(z.object({ password: passwordSchema, hint: hintSchema })) + .mutation(async ({ ctx, input }) => { + assertBrowserSession(ctx); + assertCanManageSuperPassword(ctx); + await setSuperPassword({ + userId: ctx.user.id, + password: input.password, + hint: input.hint, + }); + void sendSuperPasswordAlert({ type: "set" }, alertContext(ctx)); + await auditSuperPassword(ctx, "create", "set"); + return true; + }), + + unlock: protectedProcedure + .input(z.object({ password: passwordSchema })) + .mutation(async ({ ctx, input }) => { + assertBrowserSession(ctx); + const context = alertContext(ctx); + await verifySuperPassword({ ...context, password: input.password }); + const { expiresAt } = await openSuperSession({ + userId: ctx.user.id, + sessionId: ctx.session.id, + }); + void sendSuperPasswordAlert({ type: "opened", expiresAt }, context); + await auditSuperPassword(ctx, "start", "unlock"); + return { expiresAt }; + }), + + extend: protectedProcedure.mutation(async ({ ctx }) => { + assertBrowserSession(ctx); + const { expiresAt } = await extendSuperSession(ctx.user.id); + void sendSuperPasswordAlert( + { type: "extended", expiresAt }, + alertContext(ctx), + ); + await auditSuperPassword(ctx, "update", "extend"); + return { expiresAt }; + }), + + close: protectedProcedure.mutation(async ({ ctx }) => { + if (await closeSuperSession(ctx.user.id)) { + void sendSuperPasswordAlert( + { type: "closed", via: "panel" }, + alertContext(ctx), + ); + } + await auditSuperPassword(ctx, "stop", "close"); + return true; + }), + + change: protectedProcedure + .input( + z.object({ + currentPassword: passwordSchema, + password: passwordSchema, + hint: hintSchema, + }), + ) + .mutation(async ({ ctx, input }) => { + assertBrowserSession(ctx); + const context = alertContext(ctx); + await verifySuperPassword({ + ...context, + password: input.currentPassword, + }); + await changeSuperPassword({ + userId: ctx.user.id, + password: input.password, + hint: input.hint, + }); + void sendSuperPasswordAlert({ type: "changed" }, context); + await auditSuperPassword(ctx, "update", "change"); + return true; + }), + + disable: protectedProcedure + .input(z.object({ password: passwordSchema })) + .mutation(async ({ ctx, input }) => { + assertBrowserSession(ctx); + const context = alertContext(ctx); + await verifySuperPassword({ ...context, password: input.password }); + await removeSuperPassword(ctx.user.id); + void sendSuperPasswordAlert({ type: "disabled" }, context); + await auditSuperPassword(ctx, "delete", "disable"); + return true; + }), + + requestReset: protectedProcedure + .input(z.object({ notificationId: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + assertBrowserSession(ctx); + await requestSuperPasswordReset({ + ...alertContext(ctx), + notificationId: input.notificationId, + }); + return true; + }), + + resetWithToken: protectedProcedure + .input( + z.object({ + token: tokenSchema, + password: passwordSchema, + hint: hintSchema, + }), + ) + .mutation(async ({ ctx, input }) => { + assertBrowserSession(ctx); + await resetSuperPasswordWithToken({ + userId: ctx.user.id, + token: input.token, + password: input.password, + hint: input.hint, + }); + void sendSuperPasswordAlert( + { type: "reset", via: "link" }, + alertContext(ctx), + ); + await auditSuperPassword(ctx, "update", "reset"); + return true; + }), + + // Reached from the "This wasn't me" link without signing in; the token can + // only close access. + lockWithToken: publicProcedure + .input(z.object({ token: tokenSchema })) + .mutation(async ({ input }) => { + return { closed: await closeSuperSessionWithToken(input.token) }; + }), +}); diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 8c351a58007..afdb1f0de11 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -43,6 +43,7 @@ import { resolvePermissions, } from "@dokploy/server/services/permission"; import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; +import { matchesSuperPassword } from "@dokploy/server/services/super-password"; import { fetchWithPublicEgress } from "@dokploy/server/utils/url/network"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; @@ -501,6 +502,13 @@ export const userRouter = createTRPCRouter({ message: "New password is required", }); } + + if (await matchesSuperPassword(ctx.user.id, password)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "The login password must differ from your super password", + }); + } await db .update(account) .set({ diff --git a/apps/dokploy/server/api/trpc.ts b/apps/dokploy/server/api/trpc.ts index a3f4886a81e..9d0c388d92d 100644 --- a/apps/dokploy/server/api/trpc.ts +++ b/apps/dokploy/server/api/trpc.ts @@ -13,12 +13,14 @@ import type { statements } from "@dokploy/server/lib/access-control"; import { validateRequest } from "@dokploy/server/lib/auth"; import { checkPermission } from "@dokploy/server/services/permission"; import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; +import { getSuperSessionDenial } from "@dokploy/server/services/super-password"; import type { OpenApiMeta } from "@dokploy/trpc-openapi"; import { initTRPC, TRPCError } from "@trpc/server"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; import type { Session, User } from "better-auth"; import superjson from "superjson"; import { ZodError } from "zod"; +import { enforceProcedureSuperSession } from "@/server/api/utils/super-session"; type Resource = keyof typeof statements; type ActionOf = (typeof statements)[R][number]; @@ -41,7 +43,12 @@ interface CreateContextOptions { }) | null; session: - | (Session & { activeOrganizationId: string; impersonatedBy?: string }) + | (Session & { + activeOrganizationId: string; + impersonatedBy?: string; + authMethod?: "session" | "api-key"; + apiKeyId?: string; + }) | null; req: CreateNextContextOptions["req"]; res: CreateNextContextOptions["res"]; @@ -120,6 +127,7 @@ const t = initTRPC ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, + superSession: getSuperSessionDenial(error), }, }; }, @@ -139,6 +147,22 @@ const t = initTRPC */ export const createTRPCRouter = t.router; +const superSessionGuard = t.middleware( + async ({ ctx, path, type, getRawInput, next }) => { + if (ctx.user && ctx.session) { + await enforceProcedureSuperSession({ + ctx: { user: ctx.user, session: ctx.session }, + path, + type, + getRawInput, + }); + } + return next(); + }, +); + +const baseProcedure = t.procedure.use(superSessionGuard); + /** * Public (unauthenticated) procedure * @@ -146,7 +170,7 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const publicProcedure = t.procedure; +export const publicProcedure = baseProcedure; /** * Protected (authenticated) procedure @@ -156,7 +180,7 @@ export const publicProcedure = t.procedure; * * @see https://trpc.io/docs/procedures */ -export const protectedProcedure = t.procedure.use(({ ctx, next }) => { +export const protectedProcedure = baseProcedure.use(({ ctx, next }) => { if (!ctx.session || !ctx.user) { throw new TRPCError({ code: "UNAUTHORIZED" }); } @@ -170,7 +194,7 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => { }); }); -export const cliProcedure = t.procedure.use(({ ctx, next }) => { +export const cliProcedure = baseProcedure.use(({ ctx, next }) => { if ( !ctx.session || !ctx.user || @@ -188,7 +212,7 @@ export const cliProcedure = t.procedure.use(({ ctx, next }) => { }); }); -export const adminProcedure = t.procedure.use(({ ctx, next }) => { +export const adminProcedure = baseProcedure.use(({ ctx, next }) => { if ( !ctx.session || !ctx.user || @@ -206,7 +230,7 @@ export const adminProcedure = t.procedure.use(({ ctx, next }) => { }); }); -export const ownerProcedure = t.procedure.use(({ ctx, next }) => { +export const ownerProcedure = baseProcedure.use(({ ctx, next }) => { if (!ctx.session || !ctx.user || ctx.user.role !== "owner") { throw new TRPCError({ code: "UNAUTHORIZED" }); } @@ -223,7 +247,7 @@ export const ownerProcedure = t.procedure.use(({ ctx, next }) => { * Does NOT call the license server on every request; full validation (haveValidLicenseKey) * is used in the UI gate and when activating/validating keys. */ -export const enterpriseProcedure = t.procedure.use(async ({ ctx, next }) => { +export const enterpriseProcedure = baseProcedure.use(async ({ ctx, next }) => { if ( !ctx.session || !ctx.user || @@ -255,7 +279,7 @@ export const enterpriseProcedure = t.procedure.use(async ({ ctx, next }) => { * Requires owner role AND enterprise enabled with a license key in DB. * Use for instance-wide policy settings that the UI exposes only to owners. */ -export const enterpriseOwnerProcedure = t.procedure.use( +export const enterpriseOwnerProcedure = baseProcedure.use( async ({ ctx, next }) => { if (!ctx.session || !ctx.user || ctx.user.role !== "owner") { throw new TRPCError({ code: "UNAUTHORIZED" }); diff --git a/apps/dokploy/server/api/utils/local-docker-access.ts b/apps/dokploy/server/api/utils/local-docker-access.ts index 3cf0c9f51ea..b1138122ddd 100644 --- a/apps/dokploy/server/api/utils/local-docker-access.ts +++ b/apps/dokploy/server/api/utils/local-docker-access.ts @@ -1,4 +1,4 @@ -import { getConfig } from "@dokploy/server"; +import { execAsync, getConfig } from "@dokploy/server"; import { db } from "@dokploy/server/db"; import { applications, @@ -14,6 +14,7 @@ import { import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { quote } from "shell-quote"; type LocalDockerAccessCtx = { user: { id: string }; @@ -243,3 +244,55 @@ export const assertLocalDockerContainerAccess = async ( return config; }; + +// Containers that no Dokploy service owns are the panel's own (dokploy, its +// postgres and redis, traefik) or were started by hand on the host. +// assertSystemAccess decides whether the caller may open them. +export const assertLocalDockerContainerOrSystemAccess = async ( + ctx: LocalDockerAccessCtx, + containerId: string, + permission: LocalDockerPermission, + assertSystemAccess: () => Promise, +) => { + assertDockerIdentifier(containerId); + + const config = await getConfig(containerId, null); + const appName = getLocalDockerAppName(config); + const service = appName ? await findServiceByAppName(appName) : null; + + if (!service) { + await assertSystemAccess(); + return config; + } + if (service.serverId) { + throw unauthorizedLocalDockerTarget(); + } + + await checkServicePermissionAndAccess(ctx, service.id, { + docker: [permission], + }); + return config; +}; + +export const isLocalSystemDockerContainer = async (containerId: string) => { + assertDockerIdentifier(containerId); + const config = await getConfig(containerId, null); + const appName = getLocalDockerAppName(config); + return !appName || !(await findServiceByAppName(appName)); +}; + +export const isLocalSystemDockerVolume = async (volumeName: string) => { + assertDockerIdentifier(volumeName); + const { stdout } = await execAsync( + `docker ps -a --no-trunc --filter ${quote([`volume=${volumeName}`])} --format '{{.ID}}'`, + ); + for (const containerId of stdout.split("\n")) { + if ( + containerId.trim() && + (await isLocalSystemDockerContainer(containerId.trim())) + ) { + return true; + } + } + return false; +}; diff --git a/apps/dokploy/server/api/utils/procedure-access.ts b/apps/dokploy/server/api/utils/procedure-access.ts new file mode 100644 index 00000000000..af8cf9a170c --- /dev/null +++ b/apps/dokploy/server/api/utils/procedure-access.ts @@ -0,0 +1,762 @@ +import { db } from "@dokploy/server/db"; +import { backups, schedules } from "@dokploy/server/db/schema"; +import type { SuperSessionAccessKind } from "@dokploy/server/services/super-password"; +import { eq } from "drizzle-orm"; + +type ProcedureAccessCtx = { + user: { id: string; email?: string | null }; +}; + +type ConditionalAccess = { + dangerousWhen: ( + input: unknown, + ctx: ProcedureAccessCtx, + ) => boolean | Promise; +}; + +/** + * Super password classification of every tRPC procedure in the app router. + * + * - "read": allowed for browser sessions and API keys. + * - "write": allowed for browser sessions; API keys of a user with a super + * password need an open super session. + * - "dangerous": a user with a super password needs a browser session and an + * open super session; API keys can never call it. + * + * Users without a super password keep today's behaviour. The router test in + * __test__/security/super-session-procedure-access.test.ts fails when a + * procedure is missing here, so procedures added by upstream merges must be + * classified before they ship. Some procedures also check the super session + * themselves for part of their input (Traefik TLS files, panel system + * containers and volumes). + */ +export type ProcedureAccess = SuperSessionAccessKind | ConditionalAccess; + +const asRecord = (input: unknown) => + input && typeof input === "object" ? (input as Record) : {}; + +const changesLoginCredentials = (input: unknown, ctx: ProcedureAccessCtx) => { + const { email, password, currentPassword } = asRecord(input); + if (password || currentPassword) { + return true; + } + return ( + typeof email === "string" && + email.trim().toLowerCase() !== (ctx.user.email ?? "").trim().toLowerCase() + ); +}; + +const isHostScheduleInput = (input: unknown) => + asRecord(input).scheduleType === "dokploy-server"; + +const isStoredHostSchedule = async (input: unknown) => { + const { scheduleId } = asRecord(input); + if (typeof scheduleId !== "string") { + return false; + } + const schedule = await db.query.schedules.findFirst({ + where: eq(schedules.scheduleId, scheduleId), + columns: { scheduleType: true }, + }); + return schedule?.scheduleType === "dokploy-server"; +}; + +// A web server backup is a dump of the panel database, which stores every +// secret in clear. +const isWebServerBackupInput = (input: unknown) => + asRecord(input).databaseType === "web-server"; + +const isWebServerBackupUpdate = async (input: unknown) => { + if (isWebServerBackupInput(input)) { + return true; + } + const { backupId } = asRecord(input); + if (typeof backupId !== "string") { + return false; + } + const backup = await db.query.backups.findFirst({ + where: eq(backups.backupId, backupId), + columns: { databaseType: true }, + }); + return backup?.databaseType === "web-server"; +}; + +export const PROCEDURE_ACCESS: Record = { + "admin.setupMonitoring": "write", + "ai.analyzeLogs": "write", + "ai.create": "write", + "ai.delete": "write", + "ai.deploy": "write", + "ai.get": "read", + "ai.getAll": "read", + "ai.getCustomProviders": "read", + "ai.getEnabledProviders": "read", + "ai.getModels": "read", + "ai.one": "read", + "ai.saveCustomProviders": "write", + "ai.suggest": "write", + "ai.testConnection": "write", + "ai.update": "write", + "application.cancelDeployment": "write", + "application.cleanQueues": "write", + "application.clearDeployments": "write", + "application.create": "write", + "application.delete": "dangerous", + "application.deploy": "write", + "application.deployNginxQuickstart": "write", + "application.disconnectGitProvider": "write", + "application.dropDeployment": "write", + "application.envUpsert": "write", + "application.killBuild": "write", + "application.markRunning": "write", + "application.move": "write", + "application.one": "read", + "application.readAppMonitoring": "read", + "application.readLogs": "read", + "application.readTraefikConfig": "read", + "application.redeploy": "write", + "application.refreshToken": "write", + "application.reload": "write", + "application.revealEnvironment": "dangerous", + "application.saveBitbucketProvider": "write", + "application.saveBuildType": "write", + "application.saveDockerProvider": "write", + "application.saveEnvironment": "write", + "application.saveGitProvider": "write", + "application.saveGiteaProvider": "write", + "application.saveGithubProvider": "write", + "application.saveGitlabProvider": "write", + "application.search": "read", + "application.start": "write", + "application.stop": "write", + "application.update": "write", + "application.updateTraefikConfig": "write", + "auditLog.all": "read", + "backup.create": { dangerousWhen: isWebServerBackupInput }, + "backup.listBackupFiles": "read", + "backup.manualBackupCompose": "write", + "backup.manualBackupLibsql": "write", + "backup.manualBackupMariadb": "write", + "backup.manualBackupMongo": "write", + "backup.manualBackupMySql": "write", + "backup.manualBackupPostgres": "write", + "backup.manualBackupWebServer": "dangerous", + "backup.one": "read", + "backup.remove": "write", + "backup.restoreBackupWithLogs": "dangerous", + "backup.update": { dangerousWhen: isWebServerBackupUpdate }, + "bitbucket.bitbucketProviders": "read", + "bitbucket.create": "write", + "bitbucket.getBitbucketBranches": "read", + "bitbucket.getBitbucketRepositories": "read", + "bitbucket.one": "read", + "bitbucket.testConnection": "write", + "bitbucket.update": "write", + "certificates.all": "read", + "certificates.create": "write", + "certificates.one": "read", + "certificates.remove": "dangerous", + "certificates.update": "write", + "cluster.addManager": "dangerous", + "cluster.addWorker": "dangerous", + "cluster.getNodes": "read", + "cluster.removeWorker": "write", + "compose.cancelDeployment": "write", + "compose.cleanQueues": "write", + "compose.clearDeployments": "write", + "compose.create": "write", + "compose.delete": "dangerous", + "compose.deploy": "write", + "compose.deployExact": "write", + "compose.deployTemplate": "write", + "compose.disconnectGitProvider": "write", + "compose.env.upsert": "write", + "compose.fetchSourceType": "write", + "compose.getConvertedCompose": "read", + "compose.getDefaultCommand": "read", + "compose.getTags": "read", + "compose.import": "write", + "compose.isolatedDeployment": "write", + "compose.killBuild": "write", + "compose.loadMountsByService": "read", + "compose.loadServices": "read", + "compose.move": "write", + "compose.one": "read", + "compose.previewTemplate": "write", + "compose.processTemplate": "write", + "compose.randomizeCompose": "write", + "compose.readLogs": "read", + "compose.redeploy": "write", + "compose.refreshToken": "write", + "compose.revealEnvironment": "dangerous", + "compose.saveEnvironment": "write", + "compose.search": "read", + "compose.start": "write", + "compose.stop": "write", + "compose.templates": "read", + "compose.update": "write", + "customRole.all": "read", + "customRole.create": "dangerous", + "customRole.getStatements": "read", + "customRole.membersByRole": "read", + "customRole.remove": "dangerous", + "customRole.update": "dangerous", + "deployment.all": "read", + "deployment.allByCompose": "read", + "deployment.allByServer": "read", + "deployment.allByType": "read", + "deployment.allCentralized": "read", + "deployment.killProcess": "write", + "deployment.queueList": "read", + "deployment.readLogs": "read", + "deployment.reconcile": "write", + "deployment.removeDeployment": "write", + "destination.all": "read", + "destination.create": "write", + "destination.one": "read", + "destination.remove": "dangerous", + "destination.testConnection": "write", + "destination.update": "write", + "dnsProvider.all": "read", + "dnsProvider.create": "write", + "dnsProvider.createRecord": "write", + "dnsProvider.deleteRecord": "write", + "dnsProvider.listRecords": "read", + "dnsProvider.listZones": "read", + "dnsProvider.one": "read", + "dnsProvider.remove": "write", + "dnsProvider.testConnection": "write", + "dnsProvider.update": "write", + "dnsProvider.updateRecord": "write", + "docker.deleteContainerFile": "write", + "docker.getConfig": "dangerous", + "docker.getContainers": "read", + "docker.getContainersByAppLabel": "read", + "docker.getContainersByAppNameMatch": "read", + "docker.getEvents": "read", + "docker.getServerHealth": "read", + "docker.getServiceContainersByAppName": "read", + "docker.getStackContainersByAppName": "read", + "docker.killContainer": "write", + "docker.listContainerFiles": "write", + "docker.readContainerFile": "write", + "docker.removeContainer": "write", + "docker.restartContainer": "write", + "docker.startContainer": "write", + "docker.stopContainer": "write", + "docker.uploadFileToContainer": "write", + "docker.writeContainerFile": "write", + "dockerDiskUsage.getBuildCache": "read", + "dockerDiskUsage.getDiskUsage": "read", + "dockerDiskUsage.pruneBuildCache": "write", + "dockerImage.getImageConfig": "read", + "dockerImage.getImages": "read", + "dockerImage.removeImage": "write", + "dockerVolume.deleteVolumeFile": "write", + "dockerVolume.getVolumeConfig": "read", + "dockerVolume.getVolumes": "read", + "dockerVolume.getVolumesSize": "read", + "dockerVolume.listVolumeFiles": "write", + "dockerVolume.readVolumeFile": "write", + "dockerVolume.removeVolume": "dangerous", + "dockerVolume.writeVolumeFile": "write", + "domain.byApplicationId": "read", + "domain.byComposeId": "read", + "domain.canGenerateTraefikMeDomains": "read", + "domain.create": "write", + "domain.delete": "write", + "domain.generateDomain": "write", + "domain.one": "read", + "domain.toggleEnable": "write", + "domain.update": "write", + "domain.validateDomain": "write", + "environment.byProjectId": "read", + "environment.create": "write", + "environment.duplicate": "write", + "environment.one": "read", + "environment.remove": "dangerous", + "environment.search": "read", + "environment.update": "write", + "forwardAuth.deployOnServer": "dangerous", + "forwardAuth.disable": "dangerous", + "forwardAuth.enable": "dangerous", + "forwardAuth.getAuthDomain": "read", + "forwardAuth.listProviders": "read", + "forwardAuth.removeAuthDomain": "dangerous", + "forwardAuth.removeOnServer": "dangerous", + "forwardAuth.serverStatus": "read", + "forwardAuth.setAuthDomain": "dangerous", + "forwardAuth.status": "read", + "gitProvider.allForPermissions": "read", + "gitProvider.getAll": "read", + "gitProvider.remove": "dangerous", + "gitProvider.toggleShare": "write", + "gitea.create": "write", + "gitea.getGiteaBranches": "read", + "gitea.getGiteaRepositories": "read", + "gitea.getGiteaUrl": "read", + "gitea.giteaProviders": "read", + "gitea.one": "read", + "gitea.testConnection": "write", + "gitea.update": "write", + "github.appSetupState": "read", + "github.getGithubBranches": "read", + "github.getGithubRepositories": "read", + "github.githubProviders": "read", + "github.one": "read", + "github.testConnection": "write", + "github.update": "write", + "gitlab.create": "write", + "gitlab.getGitlabBranches": "read", + "gitlab.getGitlabRepositories": "read", + "gitlab.gitlabProviders": "read", + "gitlab.one": "read", + "gitlab.testConnection": "write", + "gitlab.update": "write", + "libsql.changeStatus": "write", + "libsql.create": "write", + "libsql.deploy": "write", + "libsql.deployWithLogs": "write", + "libsql.move": "write", + "libsql.one": "read", + "libsql.readLogs": "read", + "libsql.rebuild": "write", + "libsql.reload": "write", + "libsql.remove": "dangerous", + "libsql.revealEnvironment": "dangerous", + "libsql.saveEnvironment": "write", + "libsql.saveExternalPorts": "write", + "libsql.start": "write", + "libsql.stop": "write", + "libsql.update": "write", + "licenseKey.activate": "write", + "licenseKey.deactivate": "write", + "licenseKey.getEnterpriseSettings": "read", + "licenseKey.haveValidLicenseKey": "read", + "licenseKey.updateEnterpriseSettings": "write", + "licenseKey.validate": "write", + "mariadb.changePassword": "write", + "mariadb.changeStatus": "write", + "mariadb.create": "write", + "mariadb.deploy": "write", + "mariadb.deployWithLogs": "write", + "mariadb.move": "write", + "mariadb.one": "read", + "mariadb.readLogs": "read", + "mariadb.rebuild": "write", + "mariadb.reload": "write", + "mariadb.remove": "dangerous", + "mariadb.revealEnvironment": "dangerous", + "mariadb.saveEnvironment": "write", + "mariadb.saveExternalPort": "write", + "mariadb.search": "read", + "mariadb.start": "write", + "mariadb.stop": "write", + "mariadb.update": "write", + "mongo.changePassword": "write", + "mongo.changeStatus": "write", + "mongo.create": "write", + "mongo.deploy": "write", + "mongo.deployWithLogs": "write", + "mongo.move": "write", + "mongo.one": "read", + "mongo.readLogs": "read", + "mongo.rebuild": "write", + "mongo.reload": "write", + "mongo.remove": "dangerous", + "mongo.revealEnvironment": "dangerous", + "mongo.saveEnvironment": "write", + "mongo.saveExternalPort": "write", + "mongo.search": "read", + "mongo.start": "write", + "mongo.stop": "write", + "mongo.update": "write", + "mounts.allNamedByApplicationId": "read", + "mounts.create": "write", + "mounts.listByServiceId": "read", + "mounts.one": "read", + "mounts.remove": "write", + "mounts.update": "write", + "mysql.changePassword": "write", + "mysql.changeStatus": "write", + "mysql.create": "write", + "mysql.deploy": "write", + "mysql.deployWithLogs": "write", + "mysql.move": "write", + "mysql.one": "read", + "mysql.readLogs": "read", + "mysql.rebuild": "write", + "mysql.reload": "write", + "mysql.remove": "dangerous", + "mysql.revealEnvironment": "dangerous", + "mysql.saveEnvironment": "write", + "mysql.saveExternalPort": "write", + "mysql.search": "read", + "mysql.start": "write", + "mysql.stop": "write", + "mysql.update": "write", + "network.all": "read", + "network.create": "write", + "network.import": "write", + "network.inspect": "read", + "network.networksToSync": "read", + "network.one": "read", + "network.recreate": "write", + "network.remove": "write", + "network.resync": "write", + "notification.all": "read", + "notification.createCustom": "dangerous", + "notification.createDiscord": "dangerous", + "notification.createEmail": "dangerous", + "notification.createGotify": "dangerous", + "notification.createLark": "dangerous", + "notification.createMattermost": "dangerous", + "notification.createNtfy": "dangerous", + "notification.createPushover": "dangerous", + "notification.createResend": "dangerous", + "notification.createSlack": "dangerous", + "notification.createTeams": "dangerous", + "notification.createTelegram": "dangerous", + "notification.getEmailProviders": "read", + "notification.one": "read", + "notification.receiveNotification": "write", + "notification.remove": "dangerous", + "notification.testCustomConnection": "write", + "notification.testDiscordConnection": "write", + "notification.testEmailConnection": "write", + "notification.testGotifyConnection": "write", + "notification.testLarkConnection": "write", + "notification.testMattermostConnection": "write", + "notification.testNtfyConnection": "write", + "notification.testPushoverConnection": "write", + "notification.testResendConnection": "write", + "notification.testSlackConnection": "write", + "notification.testTeamsConnection": "write", + "notification.testTelegramConnection": "write", + "notification.updateCustom": "dangerous", + "notification.updateDiscord": "dangerous", + "notification.updateEmail": "dangerous", + "notification.updateGotify": "dangerous", + "notification.updateLark": "dangerous", + "notification.updateMattermost": "dangerous", + "notification.updateNtfy": "dangerous", + "notification.updatePushover": "dangerous", + "notification.updateResend": "dangerous", + "notification.updateSlack": "dangerous", + "notification.updateTeams": "dangerous", + "notification.updateTelegram": "dangerous", + "organization.active": "read", + "organization.all": "read", + "organization.allInvitations": "read", + "organization.create": "write", + "organization.delete": "dangerous", + "organization.inviteMember": "dangerous", + "organization.one": "read", + "organization.removeInvitation": "dangerous", + "organization.setDefault": "write", + "organization.update": "write", + "organization.updateMemberRole": "dangerous", + "overview.backups": "read", + "overview.domains": "read", + "overview.services": "read", + "patch.byEntityId": "read", + "patch.cleanPatchRepos": "write", + "patch.create": "write", + "patch.delete": "write", + "patch.ensureRepo": "write", + "patch.markFileForDeletion": "write", + "patch.one": "read", + "patch.readRepoDirectories": "read", + "patch.readRepoFile": "read", + "patch.saveFileAsPatch": "write", + "patch.toggleEnabled": "write", + "patch.update": "write", + "port.create": "write", + "port.delete": "write", + "port.one": "read", + "port.update": "write", + "postgres.changePassword": "write", + "postgres.changeStatus": "write", + "postgres.create": "write", + "postgres.deploy": "write", + "postgres.deployWithLogs": "write", + "postgres.move": "write", + "postgres.one": "read", + "postgres.readLogs": "read", + "postgres.rebuild": "write", + "postgres.reload": "write", + "postgres.remove": "dangerous", + "postgres.revealEnvironment": "dangerous", + "postgres.saveEnvironment": "write", + "postgres.saveExternalPort": "write", + "postgres.search": "read", + "postgres.start": "write", + "postgres.stop": "write", + "postgres.update": "write", + "previewDeployment.all": "read", + "previewDeployment.delete": "write", + "previewDeployment.one": "read", + "previewDeployment.redeploy": "write", + "project.all": "read", + "project.allForPermissions": "read", + "project.completeOnboarding": "write", + "project.create": "write", + "project.duplicate": "write", + "project.homeStats": "read", + "project.onboardingStatus": "read", + "project.one": "read", + "project.remove": "dangerous", + "project.search": "read", + "project.update": "write", + "redirects.create": "write", + "redirects.delete": "write", + "redirects.one": "read", + "redirects.update": "write", + "redis.changePassword": "write", + "redis.changeStatus": "write", + "redis.create": "write", + "redis.deploy": "write", + "redis.deployWithLogs": "write", + "redis.move": "write", + "redis.one": "read", + "redis.readLogs": "read", + "redis.rebuild": "write", + "redis.reload": "write", + "redis.remove": "dangerous", + "redis.revealEnvironment": "dangerous", + "redis.saveEnvironment": "write", + "redis.saveExternalPort": "write", + "redis.search": "read", + "redis.start": "write", + "redis.stop": "write", + "redis.update": "write", + "registry.all": "read", + "registry.create": "write", + "registry.one": "read", + "registry.remove": "dangerous", + "registry.testRegistry": "write", + "registry.testRegistryById": "write", + "registry.update": "write", + "rollback.delete": "write", + "rollback.rollback": "write", + "schedule.create": { dangerousWhen: isHostScheduleInput }, + "schedule.delete": "write", + "schedule.list": "read", + "schedule.one": "read", + "schedule.runManually": { dangerousWhen: isStoredHostSchedule }, + "schedule.update": { dangerousWhen: isStoredHostSchedule }, + "scim.createConnection": "dangerous", + "scim.decommissionConnection": "dangerous", + "scim.events": "read", + "scim.listConnections": "read", + "scim.revokeCredential": "dangerous", + "scim.rotateCredential": "dangerous", + "security.create": "write", + "security.delete": "write", + "security.one": "read", + "security.update": "write", + "server.all": "read", + "server.allForPermissions": "read", + "server.buildServers": "read", + "server.count": "read", + "server.create": "dangerous", + "server.getContainerProcesses": "read", + "server.getContainerResourceStats": "read", + "server.getDefaultCommand": "read", + "server.getServerMetrics": "read", + "server.getServerTime": "read", + "server.getServices": "read", + "server.one": "read", + "server.publicIp": "read", + "server.remove": "dangerous", + "server.security": "read", + "server.setup": "write", + "server.setupMonitoring": "write", + "server.setupWithLogs": "write", + "server.update": "dangerous", + "server.updateBuildsConcurrency": "write", + "server.validate": "read", + "server.withSSHKey": "read", + "settings.assignDomainServer": "dangerous", + "settings.checkGPUStatus": "read", + "settings.checkInfrastructureHealth": "read", + "settings.cleanAll": "write", + "settings.cleanAllDeploymentQueue": "write", + "settings.cleanDockerBuilder": "write", + "settings.cleanDockerPrune": "write", + "settings.cleanMonitoring": "write", + "settings.cleanSSHPrivateKey": "dangerous", + "settings.cleanStoppedContainers": "write", + "settings.cleanUnusedImages": "write", + "settings.cleanUnusedVolumes": "dangerous", + "settings.getDockerDiskUsage": "read", + "settings.getDokployCloudIps": "read", + "settings.getDokployVersion": "read", + "settings.getDokployVersionData": "read", + "settings.getIp": "read", + "settings.getLogCleanupStatus": "read", + "settings.getOpenApiDocument": "read", + "settings.getReleaseTag": "read", + "settings.getTraefikPorts": "read", + "settings.getUpdateData": "write", + "settings.getWebServerSettings": "read", + "settings.haveActivateRequests": "read", + "settings.haveTraefikDashboardPortEnabled": "read", + "settings.health": "read", + "settings.isCloud": "read", + "settings.isUserSubscribed": "read", + "settings.readDirectories": "read", + "settings.readMiddlewareTraefikConfig": "read", + "settings.readStats": "read", + "settings.readStatsLogs": "read", + "settings.readTraefikConfig": "read", + "settings.readTraefikEnv": "dangerous", + "settings.readTraefikFile": "read", + "settings.readWebServerTraefikConfig": "read", + "settings.reloadServer": "dangerous", + "settings.reloadTraefik": "write", + "settings.saveSSHPrivateKey": "dangerous", + "settings.setupGPU": "write", + "settings.toggleDashboard": "dangerous", + "settings.toggleRequests": "write", + "settings.updateBuildsConcurrency": "write", + "settings.updateDockerCleanup": "write", + "settings.updateEnforceSSO": "dangerous", + "settings.updateLogCleanup": "write", + "settings.updateMiddlewareTraefikConfig": "dangerous", + "settings.updateRemoteServersOnly": "write", + "settings.updateServer": "dangerous", + "settings.updateServerIp": "dangerous", + "settings.updateTraefikConfig": "dangerous", + "settings.updateTraefikFile": "dangerous", + "settings.updateTraefikPorts": "dangerous", + "settings.updateWebServerTraefikConfig": "dangerous", + "settings.writeTraefikEnv": "dangerous", + "sshKey.all": "read", + "sshKey.allForApps": "read", + "sshKey.create": "dangerous", + "sshKey.generate": "write", + "sshKey.one": "read", + "sshKey.remove": "dangerous", + "sshKey.update": "dangerous", + "sso.addTrustedOrigin": "dangerous", + "sso.deleteProvider": "dangerous", + "sso.enforceSSO": "read", + "sso.getTrustedOrigins": "read", + "sso.listProviders": "read", + "sso.one": "read", + "sso.register": "dangerous", + "sso.removeTrustedOrigin": "dangerous", + "sso.showSignInWithSSO": "read", + "sso.update": "dangerous", + "sso.updateTrustedOrigin": "dangerous", + "stripe.canCreateMoreServers": "read", + "stripe.createCheckoutSession": "write", + "stripe.createCustomerPortalSession": "write", + "stripe.getBillingStatus": "read", + "stripe.getCurrentPlan": "read", + "stripe.getInvoices": "read", + "stripe.getProducts": "read", + "stripe.startFreeTrial": "write", + "stripe.updateInvoiceNotifications": "write", + "stripe.upgradeSubscription": "write", + "superPassword.change": "write", + "superPassword.close": "write", + "superPassword.disable": "write", + "superPassword.extend": "write", + "superPassword.lockWithToken": "write", + "superPassword.requestReset": "write", + "superPassword.resetWithToken": "write", + "superPassword.set": "write", + "superPassword.status": "read", + "superPassword.unlock": "write", + "swarm.getAppInfos": "read", + "swarm.getContainerStats": "read", + "swarm.getNodeApps": "read", + "swarm.getNodeInfo": "read", + "swarm.getNodes": "read", + "tag.all": "read", + "tag.assignToProject": "write", + "tag.bulkAssign": "write", + "tag.create": "write", + "tag.one": "read", + "tag.remove": "write", + "tag.removeFromProject": "write", + "tag.update": "write", + "transfer.start": "write", + "user.all": "read", + "user.assignPermissions": "dangerous", + "user.checkUserOrganizations": "read", + "user.createApiKey": "dangerous", + "user.createUserWithCredentials": "dangerous", + "user.deleteAccount": "dangerous", + "user.deleteAccountByEmail": "dangerous", + "user.deleteApiKey": "dangerous", + "user.generateToken": "write", + "user.get": "read", + "user.getBackups": "read", + "user.getBookmarkedTemplates": "read", + "user.getContainerMetrics": "read", + "user.getInvitations": "read", + "user.getMetricsToken": "read", + "user.getPermissions": "read", + "user.getServerMetrics": "read", + "user.getUserByToken": "read", + "user.hasPassword": "read", + "user.haveRootAccess": "read", + "user.listPasskeys": "read", + "user.listSessions": "read", + "user.one": "read", + "user.remove": "dangerous", + "user.requestAccountDeletionCode": "write", + "user.revokeSession": "write", + "user.sendInvitation": "dangerous", + "user.session": "read", + "user.toggleTemplateBookmark": "write", + "user.update": { dangerousWhen: changesLoginCredentials }, + "vaultProvider.all": "read", + "vaultProvider.create": "write", + "vaultProvider.listSecretNames": "read", + "vaultProvider.one": "read", + "vaultProvider.remove": "write", + "vaultProvider.testConnection": "write", + "vaultProvider.update": "write", + "volumeBackups.create": "write", + "volumeBackups.delete": "write", + "volumeBackups.list": "read", + "volumeBackups.one": "read", + "volumeBackups.restoreVolumeBackupWithLogs": "dangerous", + "volumeBackups.runManually": "write", + "volumeBackups.update": "write", + "whitelabeling.get": "read", + "whitelabeling.getPublic": "read", + "whitelabeling.reset": "write", + "whitelabeling.update": "write", +}; + +export const resolveProcedureAccess = async ({ + path, + type, + getRawInput, + ctx, +}: { + path: string; + type: "query" | "mutation" | "subscription"; + getRawInput: () => Promise; + ctx: ProcedureAccessCtx; +}): Promise => { + const access = PROCEDURE_ACCESS[path]; + const byType: SuperSessionAccessKind = type === "query" ? "read" : "write"; + + // A mutation or subscription is never weaker than a write, whatever the + // table says, and unknown procedures fall back to their type. + if (access === undefined || access === "read") { + return byType; + } + if (typeof access === "string") { + return access; + } + return (await access.dangerousWhen(await getRawInput(), ctx)) + ? "dangerous" + : byType; +}; diff --git a/apps/dokploy/server/api/utils/request-meta.ts b/apps/dokploy/server/api/utils/request-meta.ts new file mode 100644 index 00000000000..f9f368ace54 --- /dev/null +++ b/apps/dokploy/server/api/utils/request-meta.ts @@ -0,0 +1,15 @@ +import type { IncomingMessage } from "node:http"; + +const firstHeader = (req: IncomingMessage | undefined, name: string) => { + const value = req?.headers?.[name]; + return (Array.isArray(value) ? value[0] : value)?.trim() || null; +}; + +export const getRequestMeta = (req: IncomingMessage | undefined) => ({ + ipAddress: + firstHeader(req, "x-real-ip") ?? + firstHeader(req, "x-forwarded-for")?.split(",")[0]?.trim() ?? + req?.socket?.remoteAddress ?? + null, + userAgent: firstHeader(req, "user-agent"), +}); diff --git a/apps/dokploy/server/api/utils/super-session.ts b/apps/dokploy/server/api/utils/super-session.ts new file mode 100644 index 00000000000..eb72c69cc73 --- /dev/null +++ b/apps/dokploy/server/api/utils/super-session.ts @@ -0,0 +1,103 @@ +import { + checkProtectedResourceAccess, + checkSuperSessionAccess, + SUPER_SESSION_MESSAGES, + type SuperSessionAccessKind, + type SuperSessionDenial, + SuperSessionError, +} from "@dokploy/server/services/super-password"; +import { TRPCError } from "@trpc/server"; +import { assertLocalHostAccess } from "@/server/api/utils/local-host-access"; +import { resolveProcedureAccess } from "@/server/api/utils/procedure-access"; + +type SessionLike = + | { + id?: string | null; + authMethod?: string | null; + activeOrganizationId?: string | null; + } + | null + | undefined; + +type SuperSessionCtx = { + user: { id: string; email?: string | null }; + session: SessionLike; +}; + +// validateRequest marks sessions built from an x-api-key header; every other +// session comes from the better-auth cookie. +export const isApiKeySession = (session: SessionLike) => + session?.authMethod === "api-key"; + +export const superSessionError = (reason: SuperSessionDenial) => + new TRPCError({ + code: "FORBIDDEN", + message: SUPER_SESSION_MESSAGES[reason], + cause: new SuperSessionError(reason), + }); + +export const assertSuperSessionAccess = async ( + ctx: SuperSessionCtx, + kind: SuperSessionAccessKind, +) => { + const denial = await checkSuperSessionAccess({ + userId: ctx.user.id, + kind, + viaApiKey: isApiKeySession(ctx.session), + }); + if (denial) { + throw superSessionError(denial); + } +}; + +export const assertBrowserSession = (ctx: SuperSessionCtx) => { + if (isApiKeySession(ctx.session) || !ctx.session?.id) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "This action is only available from a browser session.", + }); + } +}; + +// Traefik TLS files and the panel's own containers: owner/admin with a +// browser session and an open super session, whether or not the user set a +// super password. +export const assertProtectedResourceAccess = async (ctx: { + user: { id: string }; + session: SessionLike & { activeOrganizationId: string }; +}) => { + const denial = await checkProtectedResourceAccess({ + userId: ctx.user.id, + viaApiKey: isApiKeySession(ctx.session), + }); + if (denial) { + throw superSessionError(denial); + } + await assertLocalHostAccess(ctx); +}; + +export const hasProtectedResourceAccess = async ( + ctx: Parameters[0], +) => { + try { + await assertProtectedResourceAccess(ctx); + return true; + } catch { + return false; + } +}; + +export const enforceProcedureSuperSession = async ({ + ctx, + path, + type, + getRawInput, +}: { + ctx: SuperSessionCtx; + path: string; + type: "query" | "mutation" | "subscription"; + getRawInput: () => Promise; +}) => { + const kind = await resolveProcedureAccess({ path, type, getRawInput, ctx }); + await assertSuperSessionAccess(ctx, kind); +}; diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index 54d317f5de0..e0042de8194 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -7,6 +7,7 @@ import { WebSocketServer } from "ws"; import { canAccessDockerOverWss } from "./authorize"; import { canAccessDockerTerminalWebSocket } from "./docker-permission"; import { verifyWebSocketOrigin } from "./origin"; +import { rejectTerminalWithoutSuperSession } from "./super-session"; import { isValidContainerId, isValidShell, @@ -90,6 +91,10 @@ export const setupDockerContainerTerminalWebSocketServer = ( ws.close(4003, "Not authorized"); return; } + + if (await rejectTerminalWithoutSuperSession(ws, user, session)) { + return; + } try { if (serverId) { const server = await findServerById(serverId); diff --git a/apps/dokploy/server/wss/super-session.ts b/apps/dokploy/server/wss/super-session.ts new file mode 100644 index 00000000000..40e4ddb36ae --- /dev/null +++ b/apps/dokploy/server/wss/super-session.ts @@ -0,0 +1,44 @@ +import { + checkSuperSessionAccess, + SUPER_SESSION_MESSAGES, + type SuperSessionDenial, +} from "@dokploy/server/services/super-password"; +import type { WebSocket } from "ws"; + +type TerminalUser = { id: string } | null | undefined; +type TerminalSession = { authMethod?: string | null } | null | undefined; + +// Host, server and container shells are dangerous actions: a user with a super +// password needs the browser session and an open super session. +export const getTerminalSuperSessionDenial = async ( + user: TerminalUser, + session: TerminalSession, +): Promise => { + if (!user) { + return "super-session-required"; + } + try { + return await checkSuperSessionAccess({ + userId: user.id, + kind: "dangerous", + viaApiKey: session?.authMethod === "api-key", + }); + } catch (error) { + console.error("Failed to check the super session for a terminal", error); + return "super-session-required"; + } +}; + +export const rejectTerminalWithoutSuperSession = async ( + ws: Pick, + user: TerminalUser, + session: TerminalSession, +) => { + const denial = await getTerminalSuperSessionDenial(user, session); + if (!denial) { + return false; + } + ws.send(`${SUPER_SESSION_MESSAGES[denial]}\n`); + ws.close(4003, "Super password required"); + return true; +}; diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index 4ce4ed87a5b..2ef390c4579 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -13,6 +13,7 @@ import { getDockerHost } from "../utils/docker"; import { canAccessTerminalOverWss } from "./authorize"; import { verifyWebSocketOrigin } from "./origin"; import { canAccessServerTerminalWebSocket } from "./server-permission"; +import { rejectTerminalWithoutSuperSession } from "./super-session"; import { parseResizeMessage, parseTerminalSize, @@ -117,6 +118,10 @@ export const setupTerminalWebSocketServer = ( return; } + if (await rejectTerminalWithoutSuperSession(ws, user, session)) { + return; + } + let connectionDetails: ConnectConfig = {}; const isLocalServer = serverId === "local"; diff --git a/apps/dokploy/utils/api.ts b/apps/dokploy/utils/api.ts index d7f165f6acc..ac56d9105f0 100644 --- a/apps/dokploy/utils/api.ts +++ b/apps/dokploy/utils/api.ts @@ -9,6 +9,7 @@ import { createTRPCNext } from "@trpc/next"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import superjson from "superjson"; import type { AppRouter } from "@/server/api/root"; +import { superSessionLink } from "@/utils/super-session-link"; const getBaseUrl = () => { if (typeof window !== "undefined") return ""; @@ -45,6 +46,7 @@ const wsClient = getOrCreateWSClient(); const links = typeof window !== "undefined" ? [ + superSessionLink, splitLink({ condition: (op) => op.type === "subscription", true: wsLink({ diff --git a/apps/dokploy/utils/super-session-link.ts b/apps/dokploy/utils/super-session-link.ts new file mode 100644 index 00000000000..b8edee1cc44 --- /dev/null +++ b/apps/dokploy/utils/super-session-link.ts @@ -0,0 +1,50 @@ +import type { TRPCLink } from "@trpc/client"; +import { observable, type Unsubscribable } from "@trpc/server/observable"; +import { requestSuperSessionUnlock } from "@/lib/super-session-prompt"; +import type { AppRouter } from "@/server/api/root"; + +const getSuperSessionReason = (error: unknown) => { + const data = (error as { data?: { superSession?: unknown } } | null)?.data; + return data?.superSession === "super-session-required" + ? ("super-session-required" as const) + : null; +}; + +// Asks for the super password when a browser request needs an open super +// session, then retries the request once. +export const superSessionLink: TRPCLink = + () => + ({ next, op }) => + observable((observer) => { + let subscription: Unsubscribable | null = null; + let retried = false; + + const run = () => { + subscription = next(op).subscribe({ + next: (value) => observer.next(value), + error: (error) => { + const reason = getSuperSessionReason(error); + if (!reason || retried || op.type === "subscription") { + observer.error(error); + return; + } + retried = true; + void requestSuperSessionUnlock({ + reason, + key: op.path, + retries: true, + }).then((unlocked) => { + if (unlocked) { + run(); + } else { + observer.error(error); + } + }); + }, + complete: () => observer.complete(), + }); + }; + + run(); + return () => subscription?.unsubscribe(); + }); diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts index f17a124739d..abba9c13266 100644 --- a/packages/server/src/db/schema/index.ts +++ b/packages/server/src/db/schema/index.ts @@ -40,6 +40,7 @@ export * from "./session"; export * from "./shared"; export * from "./ssh-key"; export * from "./sso"; +export * from "./super-password"; export * from "./tag"; export * from "./transfer"; export * from "./user"; diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts index c5cb4a841db..805c74453aa 100644 --- a/packages/server/src/db/schema/notification.ts +++ b/packages/server/src/db/schema/notification.ts @@ -41,6 +41,7 @@ export const notifications = pgTable("notification", { dokployBackup: boolean("dokployBackup").notNull().default(false), dockerCleanup: boolean("dockerCleanup").notNull().default(false), serverThreshold: boolean("serverThreshold").notNull().default(false), + superPassword: boolean("superPassword").notNull().default(false), notificationType: notificationType("notificationType").notNull(), createdAt: text("createdAt") .notNull() @@ -281,7 +282,8 @@ export const apiCreateSlack = notificationsSchema webhookUrl: z.string().min(1), channel: z.string(), }) - .required(); + .required() + .extend({ superPassword: z.boolean().optional() }); export const apiUpdateSlack = apiCreateSlack.partial().extend({ notificationId: z.string().min(1), @@ -312,7 +314,8 @@ export const apiCreateTelegram = notificationsSchema chatId: z.string().min(1), messageThreadId: z.string(), }) - .required(); + .required() + .extend({ superPassword: z.boolean().optional() }); export const apiUpdateTelegram = apiCreateTelegram.partial().extend({ notificationId: z.string().min(1), @@ -343,7 +346,8 @@ export const apiCreateDiscord = notificationsSchema webhookUrl: z.string().min(1), decoration: z.boolean(), }) - .required(); + .required() + .extend({ superPassword: z.boolean().optional() }); export const apiUpdateDiscord = apiCreateDiscord.partial().extend({ notificationId: z.string().min(1), @@ -380,7 +384,8 @@ export const apiCreateEmail = notificationsSchema fromAddress: z.string().min(1), toAddresses: z.array(z.string()).min(1), }) - .required(); + .required() + .extend({ superPassword: z.boolean().optional() }); export const apiUpdateEmail = apiCreateEmail.partial().extend({ notificationId: z.string().min(1), @@ -415,7 +420,8 @@ export const apiCreateResend = notificationsSchema fromAddress: z.string().min(1), toAddresses: z.array(z.string()).min(1), }) - .required(); + .required() + .extend({ superPassword: z.boolean().optional() }); export const apiUpdateResend = apiCreateResend.partial().extend({ notificationId: z.string().min(1), @@ -448,7 +454,8 @@ export const apiCreateGotify = notificationsSchema priority: z.number().min(1), decoration: z.boolean(), }) - .required(); + .required() + .extend({ superPassword: z.boolean().optional() }); export const apiUpdateGotify = apiCreateGotify.partial().extend({ notificationId: z.string().min(1), @@ -485,7 +492,8 @@ export const apiCreateNtfy = notificationsSchema accessToken: z.string().optional(), priority: z.number().min(1), }) - .required(); + .required() + .extend({ superPassword: z.boolean().optional() }); export const apiUpdateNtfy = apiCreateNtfy.partial().extend({ notificationId: z.string().min(1), @@ -513,6 +521,7 @@ export const apiCreateMattermost = notificationsSchema serverThreshold: true, }) .extend({ + superPassword: z.boolean().optional(), webhookUrl: z.string().url(), channel: z.string().optional(), username: z.string().optional(), @@ -565,6 +574,7 @@ export const apiCreateCustom = notificationsSchema serverThreshold: true, }) .extend({ + superPassword: z.boolean().optional(), endpoint: z.string().min(1), headers: z.record(z.string(), z.string()).optional(), }); @@ -596,7 +606,8 @@ export const apiCreateLark = notificationsSchema .extend({ webhookUrl: z.string().min(1), }) - .required(); + .required() + .extend({ superPassword: z.boolean().optional() }); export const apiUpdateLark = apiCreateLark.partial().extend({ notificationId: z.string().min(1), @@ -624,7 +635,8 @@ export const apiCreateTeams = notificationsSchema .extend({ webhookUrl: z.string().min(1), }) - .required(); + .required() + .extend({ superPassword: z.boolean().optional() }); export const apiUpdateTeams = apiCreateTeams.partial().extend({ notificationId: z.string().min(1), @@ -650,6 +662,7 @@ export const apiCreatePushover = notificationsSchema serverThreshold: true, }) .extend({ + superPassword: z.boolean().optional(), userKey: z.string().min(1), apiToken: z.string().min(1), priority: z.number().min(-2).max(2).default(0), @@ -683,6 +696,7 @@ export const apiUpdatePushover = z.object({ appDeploy: z.boolean().optional(), dockerCleanup: z.boolean().optional(), serverThreshold: z.boolean().optional(), + superPassword: z.boolean().optional(), }); export const apiTestPushoverConnection = z diff --git a/packages/server/src/db/schema/super-password.ts b/packages/server/src/db/schema/super-password.ts new file mode 100644 index 00000000000..92e460dd5b4 --- /dev/null +++ b/packages/server/src/db/schema/super-password.ts @@ -0,0 +1,47 @@ +import { index, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { nanoid } from "nanoid"; +import { user } from "./user"; + +export const superPassword = pgTable("super_password", { + userId: text("user_id") + .primaryKey() + .references(() => user.id, { onDelete: "cascade" }), + passwordHash: text("password_hash").notNull(), + hint: text("hint"), + failedAttempts: integer("failed_attempts").notNull().default(0), + lockedUntil: timestamp("locked_until"), + resetRequestCount: integer("reset_request_count").notNull().default(0), + resetWindowStartedAt: timestamp("reset_window_started_at"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +export const superSession = pgTable("super_session", { + userId: text("user_id") + .primaryKey() + .references(() => user.id, { onDelete: "cascade" }), + expiresAt: timestamp("expires_at").notNull(), + openedAt: timestamp("opened_at").notNull(), + openedBySessionId: text("opened_by_session_id"), + lastExtendedAt: timestamp("last_extended_at"), +}); + +export type SuperPasswordTokenType = "reset" | "lock"; + +export const superPasswordToken = pgTable( + "super_password_token", + { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + type: text("type").$type().notNull(), + tokenHash: text("token_hash").notNull().unique(), + expiresAt: timestamp("expires_at").notNull(), + usedAt: timestamp("used_at"), + createdAt: timestamp("created_at").notNull().defaultNow(), + }, + (table) => [index("super_password_token_user_id_idx").on(table.userId)], +); diff --git a/packages/server/src/emails/emails/super-password.tsx b/packages/server/src/emails/emails/super-password.tsx new file mode 100644 index 00000000000..641937ed373 --- /dev/null +++ b/packages/server/src/emails/emails/super-password.tsx @@ -0,0 +1,93 @@ +import { + Body, + Button, + Container, + Head, + Heading, + Html, + Img, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; +import { emailTailwindConfig } from "../tailwind-config"; + +export type SuperPasswordEmailProps = { + title: string; + intro: string; + details?: { label: string; value: string }[]; + action?: { label: string; url: string }; + footer: string; +}; + +export const SuperPasswordEmail = ({ + title = "Super password access opened", + intro = "Your super password was used to open access to dangerous actions.", + details = [], + action, + footer = "If this wasn't you, close access and change your passwords.", +}: SuperPasswordEmailProps) => { + return ( + + + {title} + + + +
+ Dokploy +
+ +
+ + {title} + + + {intro} + + + {details.length > 0 && ( +
+ {details.map((detail) => ( + + {detail.label}: {detail.value} + + ))} +
+ )} + + {action && ( +
+ +
+ )} +
+ +
+ + {footer} + +
+
+ +
+ + ); +}; + +export default SuperPasswordEmail; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ccf7dbccb7b..25645ecc55d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -55,6 +55,7 @@ export * from "./services/server"; export * from "./services/server-health"; export * from "./services/settings"; export * from "./services/ssh-key"; +export * from "./services/super-password"; export * from "./services/transfer"; export * from "./services/user"; export * from "./services/vault-provider"; diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index c3eb572722a..96b1175fed8 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -32,6 +32,7 @@ import { import { getPublicIpWithFallback } from "../wss/utils"; import { ac, adminRole, memberRole, ownerRole } from "./access-control"; import { betterAuthSecret, scimCredentialHashSecret } from "./auth-secret"; +import { enforceSuperSessionForAuthEndpoint } from "./super-session-auth"; export const isEmailPasswordSignInPath = (path: string | undefined) => path === "/sign-in/email" || path?.endsWith("/sign-in/email"); @@ -189,6 +190,7 @@ const { handler, api } = betterAuth({ "Email and password sign-in is disabled while SSO is enforced", }); } + await enforceSuperSessionForAuthEndpoint(ctx); }), }, async trustedOrigins(request) { @@ -694,6 +696,8 @@ export const validateRequest = async (request: IncomingMessage) => { session: { userId: apiKeyRecord.user.id, activeOrganizationId: organizationId || "", + authMethod: "api-key" as const, + apiKeyId: apiKeyRecord.id, }, user: { id: userFromDb.id, @@ -758,6 +762,7 @@ export const validateRequest = async (request: IncomingMessage) => { session: { ...session.session, activeOrganizationId: member?.organization.id || "", + authMethod: "session" as const, }, user: { ...session.user, diff --git a/packages/server/src/lib/super-session-auth.ts b/packages/server/src/lib/super-session-auth.ts new file mode 100644 index 00000000000..f3e95953f91 --- /dev/null +++ b/packages/server/src/lib/super-session-auth.ts @@ -0,0 +1,93 @@ +import { APIError, getSessionFromCtx } from "better-auth/api"; +import { + checkSuperSessionAccess, + SUPER_SESSION_MESSAGES, +} from "../services/super-password"; + +// Better Auth endpoints that change who can access the account or the +// organization. Sign-in, sign-out, session refresh and 2FA verification during +// sign-in are deliberately absent so a closed super session never locks a user +// out. +export const SUPER_SESSION_AUTH_PATHS = [ + "/api-key/create", + "/api-key/update", + "/api-key/delete", + "/change-email", + "/change-password", + "/set-password", + "/delete-user", + "/delete-user/callback", + "/link-social", + "/unlink-account", + "/organization/create", + "/organization/update", + "/organization/delete", + "/organization/invite-member", + "/organization/cancel-invitation", + "/organization/remove-member", + "/organization/update-member-role", + "/organization/leave", + "/organization/create-role", + "/organization/update-role", + "/organization/delete-role", + "/organization/create-team", + "/organization/update-team", + "/organization/remove-team", + "/organization/add-team-member", + "/organization/remove-team-member", + "/two-factor/enable", + "/two-factor/disable", + "/two-factor/generate-backup-codes", + "/two-factor/get-totp-uri", + "/passkey/generate-register-options", + "/passkey/verify-registration", + "/passkey/update-passkey", + "/passkey/delete-passkey", + "/sso/register", + "/sso/update-provider", + "/sso/delete-provider", + "/sso/request-domain-verification", + "/sso/verify-domain", +] as const; + +const gatedAuthPaths = new Set(SUPER_SESSION_AUTH_PATHS); + +export const SUPER_SESSION_AUTH_ERROR_CODE = "SUPER_SESSION_REQUIRED"; + +export const isSuperSessionAuthPath = (path: string | undefined) => { + if (!path) { + return false; + } + const normalized = path.replace(/^\/api\/auth(?=\/)/, "").replace(/\/+$/, ""); + return gatedAuthPaths.has(normalized); +}; + +type AuthEndpointContext = Parameters[0]; + +export const enforceSuperSessionForAuthEndpoint = async ( + ctx: AuthEndpointContext, +) => { + if (!isSuperSessionAuthPath(ctx.path)) { + return; + } + + const session = await getSessionFromCtx(ctx, { disableRefresh: true }).catch( + () => null, + ); + const userId = session?.user?.id; + if (!userId) { + return; + } + + const denial = await checkSuperSessionAccess({ + userId, + kind: "dangerous", + viaApiKey: false, + }); + if (denial) { + throw new APIError("FORBIDDEN", { + message: SUPER_SESSION_MESSAGES[denial], + code: SUPER_SESSION_AUTH_ERROR_CODE, + }); + } +}; diff --git a/packages/server/src/services/notification.ts b/packages/server/src/services/notification.ts index f95cf26c6bd..fe3f143151a 100644 --- a/packages/server/src/services/notification.ts +++ b/packages/server/src/services/notification.ts @@ -120,6 +120,7 @@ export const createSlackNotification = async ( notificationType: "slack", organizationId: organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -152,6 +153,7 @@ export const updateSlackNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -218,6 +220,7 @@ export const createTelegramNotification = async ( notificationType: "telegram", organizationId: organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -250,6 +253,7 @@ export const updateTelegramNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -315,6 +319,7 @@ export const createDiscordNotification = async ( notificationType: "discord", organizationId: organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -347,6 +352,7 @@ export const updateDiscordNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -416,6 +422,7 @@ export const createEmailNotification = async ( notificationType: "email", organizationId: organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -468,6 +475,7 @@ export const updateEmailNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -535,6 +543,7 @@ export const createResendNotification = async ( notificationType: "resend", organizationId: organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -567,6 +576,7 @@ export const updateResendNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -632,6 +642,7 @@ export const createGotifyNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, notificationType: "gotify", organizationId: organizationId, }) @@ -684,6 +695,7 @@ export const updateGotifyNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, organizationId: input.organizationId, }) .where(eq(notifications.notificationId, input.notificationId)) @@ -749,6 +761,7 @@ export const createNtfyNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, notificationType: "ntfy", organizationId: organizationId, }) @@ -801,6 +814,7 @@ export const updateNtfyNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, organizationId: input.organizationId, }) .where(eq(notifications.notificationId, input.notificationId)) @@ -867,6 +881,7 @@ export const createCustomNotification = async ( notificationType: "custom", organizationId: organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -919,6 +934,7 @@ export const updateCustomNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -1016,6 +1032,7 @@ export const createLarkNotification = async ( notificationType: "lark", organizationId: organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -1048,6 +1065,7 @@ export const updateLarkNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -1113,6 +1131,7 @@ export const createTeamsNotification = async ( notificationType: "teams", organizationId: organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -1145,6 +1164,7 @@ export const updateTeamsNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -1227,6 +1247,7 @@ export const createMattermostNotification = async ( notificationType: "mattermost", organizationId: organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -1259,6 +1280,7 @@ export const updateMattermostNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -1326,6 +1348,7 @@ export const createPushoverNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, notificationType: "pushover", organizationId: organizationId, }) @@ -1360,6 +1383,7 @@ export const updatePushoverNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, + superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() diff --git a/packages/server/src/services/super-password.ts b/packages/server/src/services/super-password.ts new file mode 100644 index 00000000000..ad98f195515 --- /dev/null +++ b/packages/server/src/services/super-password.ts @@ -0,0 +1,1073 @@ +import { createHash, randomBytes } from "node:crypto"; +import { db } from "@dokploy/server/db"; +import { + account, + member, + notifications, + type SuperPasswordTokenType, + superPassword, + superPasswordToken, + superSession, + user as userTable, +} from "@dokploy/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import bcrypt from "bcrypt"; +import { and, eq, gt, isNotNull, isNull, lt, or, sql } from "drizzle-orm"; +import { + type SuperPasswordAlertMessage, + type SuperPasswordChannel, + sendSuperPasswordAlertToChannel, +} from "../utils/notifications/super-password"; +import { getDokployUrl } from "./admin"; + +export const SUPER_SESSION_DURATION_MS = 24 * 60 * 60 * 1000; +export const SUPER_PASSWORD_MAX_FAILED_ATTEMPTS = 5; +export const SUPER_PASSWORD_LOCK_DURATION_MS = 15 * 60 * 1000; +export const SUPER_PASSWORD_RESET_TOKEN_TTL_MS = 30 * 60 * 1000; +export const SUPER_PASSWORD_LOCK_TOKEN_TTL_MS = SUPER_SESSION_DURATION_MS; +export const SUPER_PASSWORD_MIN_LENGTH = 8; +// bcrypt ignores everything after the first 72 bytes. +export const SUPER_PASSWORD_MAX_BYTES = 72; +export const SUPER_PASSWORD_HINT_MAX_LENGTH = 200; +const SUPER_PASSWORD_HASH_ROUNDS = 12; + +export const SUPER_PASSWORD_PROFILE_PATH = "/dashboard/settings/profile"; +export const SUPER_PASSWORD_RESET_PATH = + "/dashboard/settings/super-password-reset"; +export const SUPER_PASSWORD_LOCK_PATH = "/super-password/lock"; + +export type SuperSessionAccessKind = "read" | "write" | "dangerous"; + +export type SuperSessionDenial = + | "api-key-write-locked" + | "super-session-required" + | "browser-session-required"; + +export const SUPER_SESSION_MESSAGES: Record = { + "api-key-write-locked": + "API key write access is locked. Open it in Profile → Super password.", + "super-session-required": + "This action needs the super password. Open access in Profile → Super password.", + "browser-session-required": + "This action needs a browser session with super password access open. API keys cannot perform it.", +}; + +export class SuperSessionError extends Error { + readonly reason: SuperSessionDenial; + + constructor(reason: SuperSessionDenial) { + super(SUPER_SESSION_MESSAGES[reason]); + this.name = "SuperSessionError"; + this.reason = reason; + } +} + +// Matches by name because Next and esbuild may load this module more than +// once, which breaks instanceof across copies. +export const getSuperSessionDenial = ( + error: unknown, +): SuperSessionDenial | null => { + const cause = + error && typeof error === "object" && "cause" in error + ? (error as { cause?: unknown }).cause + : error; + if ( + cause && + typeof cause === "object" && + (cause as { name?: unknown }).name === "SuperSessionError" + ) { + const reason = (cause as { reason?: unknown }).reason; + if (typeof reason === "string" && reason in SUPER_SESSION_MESSAGES) { + return reason as SuperSessionDenial; + } + } + return null; +}; + +export type SuperSessionState = { + isSet: boolean; + hint: string | null; + active: boolean; + expiresAt: Date | null; + lockedUntil: Date | null; +}; + +export type SuperPasswordUser = { id: string; email: string }; + +const badRequest = (message: string) => + new TRPCError({ code: "BAD_REQUEST", message }); + +const findSuperPassword = (userId: string) => + db.query.superPassword.findFirst({ + where: eq(superPassword.userId, userId), + }); + +export const getSuperSessionState = async ( + userId: string, + now = new Date(), +): Promise => { + const record = await findSuperPassword(userId); + if (!record) { + return { + isSet: false, + hint: null, + active: false, + expiresAt: null, + lockedUntil: null, + }; + } + + const session = await db.query.superSession.findFirst({ + where: eq(superSession.userId, userId), + }); + const active = !!session && session.expiresAt > now; + + return { + isSet: true, + hint: record.hint ?? null, + active, + expiresAt: active ? session.expiresAt : null, + lockedUntil: + record.lockedUntil && record.lockedUntil > now + ? record.lockedUntil + : null, + }; +}; + +export const evaluateSuperSessionAccess = ({ + state, + kind, + viaApiKey, +}: { + state: Pick; + kind: SuperSessionAccessKind; + viaApiKey: boolean; +}): SuperSessionDenial | null => { + if (!state.isSet || kind === "read") { + return null; + } + if (kind === "dangerous") { + if (viaApiKey) { + return "browser-session-required"; + } + return state.active ? null : "super-session-required"; + } + return viaApiKey && !state.active ? "api-key-write-locked" : null; +}; + +export const checkSuperSessionAccess = async ({ + userId, + kind, + viaApiKey, +}: { + userId: string; + kind: SuperSessionAccessKind; + viaApiKey: boolean; +}) => { + if (kind === "read" || (kind === "write" && !viaApiKey)) { + return null; + } + const state = await getSuperSessionState(userId); + return evaluateSuperSessionAccess({ state, kind, viaApiKey }); +}; + +// TLS keys and the panel's own containers were hard-denied before the super +// password existed, so without an open super session they stay denied even for +// users who never set one. +export const checkProtectedResourceAccess = async ({ + userId, + viaApiKey, +}: { + userId: string; + viaApiKey: boolean; +}): Promise => { + if (viaApiKey) { + return "browser-session-required"; + } + const state = await getSuperSessionState(userId); + return state.isSet && state.active ? null : "super-session-required"; +}; + +export const validateSuperPassword = (password: string) => { + if ( + typeof password !== "string" || + password.length < SUPER_PASSWORD_MIN_LENGTH + ) { + throw badRequest( + `The super password must be at least ${SUPER_PASSWORD_MIN_LENGTH} characters long`, + ); + } + if (Buffer.byteLength(password, "utf8") > SUPER_PASSWORD_MAX_BYTES) { + throw badRequest( + `The super password must be at most ${SUPER_PASSWORD_MAX_BYTES} bytes long`, + ); + } +}; + +export const normalizeSuperPasswordHint = ( + hint: string | null | undefined, + password: string, +) => { + const normalized = hint?.trim() ?? ""; + if (!normalized) { + return null; + } + if (normalized.length > SUPER_PASSWORD_HINT_MAX_LENGTH) { + throw badRequest( + `The hint must be at most ${SUPER_PASSWORD_HINT_MAX_LENGTH} characters long`, + ); + } + if (normalized.toLowerCase().includes(password.toLowerCase())) { + throw badRequest("The hint must not contain the super password"); + } + return normalized; +}; + +const compareHash = async (password: string, hash: string | null) => { + if (!hash) { + return false; + } + try { + return await bcrypt.compare(password, hash); + } catch { + return false; + } +}; + +export const matchesLoginPassword = async ( + userId: string, + password: string, +) => { + const credential = await db.query.account.findFirst({ + where: and( + eq(account.userId, userId), + eq(account.providerId, "credential"), + ), + columns: { password: true }, + }); + return compareHash(password, credential?.password ?? null); +}; + +export const matchesSuperPassword = async ( + userId: string, + password: string, +) => { + const record = await findSuperPassword(userId); + return compareHash(password, record?.passwordHash ?? null); +}; + +const prepareSuperPassword = async ( + userId: string, + password: string, + hint: string | null | undefined, +) => { + validateSuperPassword(password); + const normalizedHint = normalizeSuperPasswordHint(hint, password); + if (await matchesLoginPassword(userId, password)) { + throw badRequest("The super password must differ from your login password"); + } + return { + passwordHash: await bcrypt.hash(password, SUPER_PASSWORD_HASH_ROUNDS), + hint: normalizedHint, + }; +}; + +export const setSuperPassword = async ({ + userId, + password, + hint, +}: { + userId: string; + password: string; + hint?: string | null; +}) => { + if (await findSuperPassword(userId)) { + throw new TRPCError({ + code: "CONFLICT", + message: + "A super password is already set. Change it with the current super password.", + }); + } + const prepared = await prepareSuperPassword(userId, password, hint); + await db.insert(superPassword).values({ userId, ...prepared }); +}; + +export const changeSuperPassword = async ({ + userId, + password, + hint, +}: { + userId: string; + password: string; + hint?: string | null; +}) => { + const prepared = await prepareSuperPassword(userId, password, hint); + await db + .update(superPassword) + .set({ + ...prepared, + failedAttempts: 0, + lockedUntil: null, + updatedAt: new Date(), + }) + .where(eq(superPassword.userId, userId)); +}; + +const lockedError = (lockedUntil: Date) => + new TRPCError({ + code: "TOO_MANY_REQUESTS", + message: `Too many wrong super password attempts. Try again after ${lockedUntil.toISOString()}.`, + }); + +export const verifySuperPassword = async ({ + user, + password, + ipAddress, + userAgent, + now = new Date(), +}: SuperPasswordAlertContext & { + user: SuperPasswordUser; + password: string; +}) => { + const record = await findSuperPassword(user.id); + if (!record) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: "No super password is set", + }); + } + if (record.lockedUntil && record.lockedUntil > now) { + throw lockedError(record.lockedUntil); + } + + if (await compareHash(password, record.passwordHash)) { + if (record.failedAttempts > 0 || record.lockedUntil) { + await db + .update(superPassword) + .set({ failedAttempts: 0, lockedUntil: null, updatedAt: now }) + .where(eq(superPassword.userId, user.id)); + } + return; + } + + const [updated] = await db + .update(superPassword) + .set({ + failedAttempts: sql`${superPassword.failedAttempts} + 1`, + updatedAt: now, + }) + .where(eq(superPassword.userId, user.id)) + .returning({ failedAttempts: superPassword.failedAttempts }); + const attempts = + updated?.failedAttempts ?? SUPER_PASSWORD_MAX_FAILED_ATTEMPTS; + + if (attempts >= SUPER_PASSWORD_MAX_FAILED_ATTEMPTS) { + const lockedUntil = new Date( + now.getTime() + SUPER_PASSWORD_LOCK_DURATION_MS, + ); + await db + .update(superPassword) + .set({ failedAttempts: 0, lockedUntil, updatedAt: now }) + .where(eq(superPassword.userId, user.id)); + void sendSuperPasswordAlert( + { type: "locked", lockedUntil }, + { user, ipAddress, userAgent, now }, + ); + throw lockedError(lockedUntil); + } + + const left = SUPER_PASSWORD_MAX_FAILED_ATTEMPTS - attempts; + void sendSuperPasswordAlert( + { type: "wrong-attempt", attemptsLeft: left }, + { user, ipAddress, userAgent, now }, + ); + throw badRequest( + `Incorrect super password. ${left} attempt${left === 1 ? "" : "s"} left before a ${SUPER_PASSWORD_LOCK_DURATION_MS / 60_000}-minute lock.`, + ); +}; + +export const openSuperSession = async ({ + userId, + sessionId, + now = new Date(), +}: { + userId: string; + sessionId: string | null; + now?: Date; +}) => { + const expiresAt = new Date(now.getTime() + SUPER_SESSION_DURATION_MS); + const values = { + expiresAt, + openedAt: now, + openedBySessionId: sessionId, + lastExtendedAt: null, + }; + await db + .insert(superSession) + .values({ userId, ...values }) + .onConflictDoUpdate({ target: superSession.userId, set: values }); + return { expiresAt }; +}; + +export const extendSuperSession = async (userId: string, now = new Date()) => { + const expiresAt = new Date(now.getTime() + SUPER_SESSION_DURATION_MS); + const [updated] = await db + .update(superSession) + .set({ expiresAt, lastExtendedAt: now }) + .where( + and(eq(superSession.userId, userId), gt(superSession.expiresAt, now)), + ) + .returning({ expiresAt: superSession.expiresAt }); + if (!updated) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: + "Super password access is closed. Open it again with the super password.", + }); + } + return { expiresAt: updated.expiresAt }; +}; + +export const closeSuperSession = async (userId: string) => { + const closed = await db + .delete(superSession) + .where(eq(superSession.userId, userId)) + .returning({ userId: superSession.userId }); + return closed.length > 0; +}; + +export const removeSuperPassword = async (userId: string) => { + await db.delete(superSession).where(eq(superSession.userId, userId)); + await db + .delete(superPasswordToken) + .where(eq(superPasswordToken.userId, userId)); + await db.delete(superPassword).where(eq(superPassword.userId, userId)); +}; + +const hashToken = (token: string) => + createHash("sha256").update(token).digest("hex"); + +export const deleteStaleSuperPasswordTokens = async (now = new Date()) => { + await db + .delete(superPasswordToken) + .where( + or( + lt(superPasswordToken.expiresAt, now), + isNotNull(superPasswordToken.usedAt), + ), + ); +}; + +export const createSuperPasswordToken = async ({ + userId, + type, + now = new Date(), +}: { + userId: string; + type: SuperPasswordTokenType; + now?: Date; +}) => { + await deleteStaleSuperPasswordTokens(now); + if (type === "reset") { + await db + .delete(superPasswordToken) + .where( + and( + eq(superPasswordToken.userId, userId), + eq(superPasswordToken.type, "reset"), + ), + ); + } + + const token = randomBytes(32).toString("base64url"); + const ttl = + type === "reset" + ? SUPER_PASSWORD_RESET_TOKEN_TTL_MS + : SUPER_PASSWORD_LOCK_TOKEN_TTL_MS; + const expiresAt = new Date(now.getTime() + ttl); + await db.insert(superPasswordToken).values({ + userId, + type, + tokenHash: hashToken(token), + expiresAt, + }); + return { token, expiresAt }; +}; + +export const consumeSuperPasswordToken = async ({ + token, + type, + userId, + now = new Date(), +}: { + token: string; + type: SuperPasswordTokenType; + userId?: string; + now?: Date; +}) => { + if (typeof token !== "string" || token.length === 0 || token.length > 256) { + return null; + } + const [consumed] = await db + .update(superPasswordToken) + .set({ usedAt: now }) + .where( + and( + eq(superPasswordToken.tokenHash, hashToken(token)), + eq(superPasswordToken.type, type), + isNull(superPasswordToken.usedAt), + gt(superPasswordToken.expiresAt, now), + ...(userId ? [eq(superPasswordToken.userId, userId)] : []), + ), + ) + .returning({ userId: superPasswordToken.userId }); + return consumed?.userId ?? null; +}; + +export const closeSuperSessionWithToken = async (token: string) => { + const userId = await consumeSuperPasswordToken({ token, type: "lock" }); + if (!userId) { + return false; + } + if (await closeSuperSession(userId)) { + void sendSuperPasswordAlert( + { type: "closed", via: "link" }, + { user: { id: userId } }, + ); + } + return true; +}; + +export const resetSuperPasswordWithToken = async ({ + userId, + token, + password, + hint, +}: { + userId: string; + token: string; + password: string; + hint?: string | null; +}) => { + const prepared = await prepareSuperPassword(userId, password, hint); + const owner = await consumeSuperPasswordToken({ + token, + type: "reset", + userId, + }); + if (!owner) { + throw badRequest("This reset link is invalid, expired or already used."); + } + const now = new Date(); + const values = { ...prepared, failedAttempts: 0, lockedUntil: null }; + await db + .insert(superPassword) + .values({ userId, ...values }) + .onConflictDoUpdate({ + target: superPassword.userId, + set: { ...values, updatedAt: now }, + }); + await closeSuperSession(userId); +}; + +export const SUPER_PASSWORD_RESET_REQUESTS_PER_HOUR = 3; +const RESET_REQUEST_WINDOW_MS = 60 * 60 * 1000; + +const CHANNEL_RELATIONS = { + slack: true, + telegram: true, + discord: true, + email: true, + resend: true, + gotify: true, + ntfy: true, + mattermost: true, + custom: true, + lark: true, + pushover: true, + teams: true, +} as const; + +export const findSuperPasswordChannelsInOrganization = async ( + organizationId: string, +): Promise => { + const channels = await db.query.notifications.findMany({ + where: and( + eq(notifications.organizationId, organizationId), + eq(notifications.superPassword, true), + ), + with: CHANNEL_RELATIONS, + }); + return channels.filter((channel) => channel.superPassword === true); +}; + +const getSuperPasswordOrganizationIds = async (userId: string) => { + const memberships = await db.query.member.findMany({ + where: eq(member.userId, userId), + columns: { organizationId: true, role: true }, + }); + return [ + ...new Set( + memberships + .filter(({ role }) => role === "owner" || role === "admin") + .map(({ organizationId }) => organizationId), + ), + ]; +}; + +export const findSuperPasswordChannels = async (userId: string) => { + const channels: SuperPasswordChannel[] = []; + for (const organizationId of await getSuperPasswordOrganizationIds(userId)) { + channels.push( + ...(await findSuperPasswordChannelsInOrganization(organizationId)), + ); + } + return channels; +}; + +export const listSuperPasswordRecoveryChannels = async (userId: string) => + (await findSuperPasswordChannels(userId)).map( + ({ notificationId, name, notificationType }) => ({ + notificationId, + name, + notificationType, + }), + ); + +export type SuperPasswordAlertContext = { + user: { id: string; email?: string | null }; + ipAddress?: string | null; + userAgent?: string | null; + now?: Date; +}; + +export type SuperPasswordChannelChange = + | "turned-on" + | "turned-off" + | "destination-changed"; + +export type SuperPasswordEvent = + | { type: "opened"; expiresAt: Date } + | { type: "extended"; expiresAt: Date } + | { type: "closed"; via: "panel" | "link" | "ssh" } + | { type: "wrong-attempt"; attemptsLeft: number } + | { type: "locked"; lockedUntil: Date } + | { type: "set" } + | { type: "changed" } + | { type: "disabled" } + | { type: "reset"; via: "link" | "ssh" } + | { type: "reset-requested"; channelName: string } + | { type: "reset-link"; url: string } + | { + type: "channel-changed"; + channelName: string; + channelType: string; + change: SuperPasswordChannelChange; + }; + +const formatTime = (date: Date) => + `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`; + +const formatDuration = (ms: number) => { + const totalMinutes = Math.max(0, Math.floor(ms / 60_000)); + return `${Math.floor(totalMinutes / 60)} h ${totalMinutes % 60} min`; +}; + +const CLOSED_VIA = { + panel: { + label: "Panel", + summary: "Access was closed from the panel.", + }, + link: { + label: "Link", + summary: 'Access was closed with the "This wasn\'t me" link.', + }, + ssh: { + label: "SSH", + summary: "Access was closed over SSH (lock-super-session).", + }, +} as const; + +const CHANNEL_CHANGES: Record = { + "turned-on": "now receives super password alerts", + "turned-off": + "no longer receives super password alerts (toggle off or channel removed)", + "destination-changed": + "changed its destination and still receives super password alerts", +}; + +export const SUPER_PASSWORD_LOCK_ACTION_LABEL = "This wasn't me – close access"; + +export const buildSuperPasswordAlertMessage = ( + event: SuperPasswordEvent, + context: SuperPasswordAlertContext & { now: Date }, + lockUrl?: string, +): SuperPasswordAlertMessage => { + const { now } = context; + const details = [ + { label: "User", value: context.user.email || context.user.id }, + { label: "Time", value: formatTime(now) }, + ]; + const requestDetails = [ + ...(context.ipAddress + ? [{ label: "IP address", value: context.ipAddress }] + : []), + ...(context.userAgent + ? [{ label: "User agent", value: context.userAgent }] + : []), + ]; + const lockAction = lockUrl + ? { label: SUPER_PASSWORD_LOCK_ACTION_LABEL, url: lockUrl } + : undefined; + + switch (event.type) { + case "opened": + case "extended": + return { + event: event.type, + title: + event.type === "opened" + ? "Super password access opened" + : "Super password access extended", + summary: + event.type === "opened" + ? "Dangerous actions and API key writes are open." + : "Access was extended for another 24 hours without the password.", + details: [ + ...details, + { + label: "Time left", + value: formatDuration(event.expiresAt.getTime() - now.getTime()), + }, + { label: "Expires", value: formatTime(event.expiresAt) }, + ...requestDetails, + ], + action: lockAction, + }; + case "closed": + return { + event: event.type, + title: "Super password access closed", + summary: CLOSED_VIA[event.via].summary, + details: [ + ...details, + { label: "Closed via", value: CLOSED_VIA[event.via].label }, + ...requestDetails, + ], + }; + case "wrong-attempt": + return { + event: event.type, + title: "Wrong super password entered", + summary: `${event.attemptsLeft} attempt${event.attemptsLeft === 1 ? "" : "s"} left before a ${SUPER_PASSWORD_LOCK_DURATION_MS / 60_000}-minute lock.`, + details: [...details, ...requestDetails], + }; + case "locked": + return { + event: event.type, + title: "Super password locked", + summary: `${SUPER_PASSWORD_MAX_FAILED_ATTEMPTS} wrong attempts. Unlocking is blocked until ${formatTime(event.lockedUntil)}.`, + details: [ + ...details, + { label: "Locked until", value: formatTime(event.lockedUntil) }, + ...requestDetails, + ], + }; + case "set": + return { + event: event.type, + title: "Super password set", + summary: + "A super password now protects dangerous actions and API key writes.", + details: [...details, ...requestDetails], + }; + case "changed": + return { + event: event.type, + title: "Super password changed", + summary: "The super password was changed with the current one.", + details: [...details, ...requestDetails], + }; + case "disabled": + return { + event: event.type, + title: "Super password turned off", + summary: + "API keys can write again and dangerous actions no longer ask for a second password. Open access was closed.", + details: [...details, ...requestDetails], + }; + case "reset": + return { + event: event.type, + title: "Super password reset", + summary: + event.via === "link" + ? "The super password was reset with a reset link. Open access was closed." + : "The super password was removed over SSH (reset-super-password). Open access was closed.", + details: [...details, ...requestDetails], + }; + case "reset-requested": + return { + event: event.type, + title: "Super password reset link requested", + summary: `A one-time reset link was sent to "${event.channelName}".`, + details: [...details, ...requestDetails], + }; + case "reset-link": + return { + event: event.type, + title: "Reset your super password", + summary: `Open the link while signed in to the panel as ${context.user.email || "the same user"}. It works once and expires in ${SUPER_PASSWORD_RESET_TOKEN_TTL_MS / 60_000} minutes. Resetting closes open access.`, + details: [...details, ...requestDetails], + action: { label: "Reset super password", url: event.url }, + }; + case "channel-changed": + return { + event: event.type, + title: "Super password alert channel changed", + summary: `"${event.channelName}" (${event.channelType}) ${CHANNEL_CHANGES[event.change]}.`, + details: [ + { label: "Changed by", value: context.user.email || context.user.id }, + { label: "Time", value: formatTime(now) }, + ...requestDetails, + ], + }; + } +}; + +const resolveAlertUser = async (user: SuperPasswordAlertContext["user"]) => { + if (user.email) { + return user; + } + const record = await db.query.user + .findFirst({ where: eq(userTable.id, user.id), columns: { email: true } }) + .catch(() => undefined); + return { ...user, email: record?.email ?? null }; +}; + +const sendToChannels = async ( + channels: SuperPasswordChannel[], + message: SuperPasswordAlertMessage, +) => { + const results = await Promise.allSettled( + channels.map((channel) => + sendSuperPasswordAlertToChannel(channel, message), + ), + ); + results.forEach((result, index) => { + if (result.status === "rejected") { + console.error( + `Failed to send a super password alert to "${channels[index]?.name}"`, + result.reason instanceof Error ? result.reason.message : result.reason, + ); + } + }); +}; + +// Never throws: alerts are best effort and must not block unlock, extend or +// any other super password action. +export const sendSuperPasswordAlert = async ( + event: SuperPasswordEvent, + context: SuperPasswordAlertContext, + options: { excludeNotificationIds?: string[] } = {}, +) => { + try { + const now = context.now ?? new Date(); + const channels = (await findSuperPasswordChannels(context.user.id)).filter( + ({ notificationId }) => + !options.excludeNotificationIds?.includes(notificationId), + ); + if (channels.length === 0) { + return; + } + let lockUrl: string | undefined; + if (event.type === "opened" || event.type === "extended") { + const { token } = await createSuperPasswordToken({ + userId: context.user.id, + type: "lock", + now, + }); + lockUrl = `${await getDokployUrl()}${SUPER_PASSWORD_LOCK_PATH}?token=${encodeURIComponent(token)}`; + } + const user = await resolveAlertUser(context.user); + await sendToChannels( + channels, + buildSuperPasswordAlertMessage(event, { ...context, user, now }, lockUrl), + ); + } catch (error) { + console.error("Failed to send super password alerts", error); + } +}; + +const channelDestination = (channel: SuperPasswordChannel) => + JSON.stringify(channel[channel.notificationType] ?? null); + +export const notifySuperPasswordChannelChanges = async ({ + before, + after, + context, +}: { + before: SuperPasswordChannel[]; + after: SuperPasswordChannel[]; + context: SuperPasswordAlertContext; +}) => { + try { + const beforeById = new Map( + before.map((channel) => [channel.notificationId, channel]), + ); + const afterIds = new Set(after.map(({ notificationId }) => notificationId)); + const changes: { + channel: SuperPasswordChannel; + change: SuperPasswordChannelChange; + }[] = []; + + for (const channel of after) { + const previous = beforeById.get(channel.notificationId); + if (!previous) { + changes.push({ channel, change: "turned-on" }); + } else if (channelDestination(previous) !== channelDestination(channel)) { + changes.push({ channel, change: "destination-changed" }); + } + } + for (const channel of before) { + if (!afterIds.has(channel.notificationId)) { + changes.push({ channel, change: "turned-off" }); + } + } + if (changes.length === 0) { + return; + } + + // The old destination of a channel that was switched off or redirected + // hears about it too, so a hijacked channel cannot go quiet unnoticed. + const recipients = [ + ...after, + ...changes + .filter(({ change }) => change !== "turned-on") + .flatMap(({ channel }) => { + const previous = beforeById.get(channel.notificationId); + return previous ? [previous] : []; + }), + ]; + const now = context.now ?? new Date(); + for (const { channel, change } of changes) { + await sendToChannels( + recipients, + buildSuperPasswordAlertMessage( + { + type: "channel-changed", + channelName: channel.name, + channelType: channel.notificationType, + change, + }, + { ...context, now }, + ), + ); + } + } catch (error) { + console.error("Failed to send super password channel alerts", error); + } +}; + +const consumeResetRequestQuota = async ( + record: NonNullable>>, + now: Date, +) => { + const windowOpen = + record.resetWindowStartedAt && + now.getTime() - record.resetWindowStartedAt.getTime() < + RESET_REQUEST_WINDOW_MS; + if ( + windowOpen && + record.resetRequestCount >= SUPER_PASSWORD_RESET_REQUESTS_PER_HOUR + ) { + throw new TRPCError({ + code: "TOO_MANY_REQUESTS", + message: `At most ${SUPER_PASSWORD_RESET_REQUESTS_PER_HOUR} reset links per hour. Try again later or use SSH.`, + }); + } + await db + .update(superPassword) + .set( + windowOpen + ? { resetRequestCount: record.resetRequestCount + 1 } + : { resetRequestCount: 1, resetWindowStartedAt: now }, + ) + .where(eq(superPassword.userId, record.userId)); +}; + +export const requestSuperPasswordReset = async ({ + user, + notificationId, + ipAddress, + userAgent, + now = new Date(), +}: SuperPasswordAlertContext & { + user: SuperPasswordUser; + notificationId: string; +}) => { + const record = await findSuperPassword(user.id); + if (!record) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: "No super password is set", + }); + } + const channel = (await findSuperPasswordChannels(user.id)).find( + (candidate) => candidate.notificationId === notificationId, + ); + if (!channel) { + throw badRequest( + "Pick a notification channel that has the 'Super password' toggle on.", + ); + } + await consumeResetRequestQuota(record, now); + + const { token } = await createSuperPasswordToken({ + userId: user.id, + type: "reset", + now, + }); + const url = `${await getDokployUrl()}${SUPER_PASSWORD_RESET_PATH}?token=${encodeURIComponent(token)}`; + const context = { user, ipAddress, userAgent, now }; + try { + await sendSuperPasswordAlertToChannel( + channel, + buildSuperPasswordAlertMessage({ type: "reset-link", url }, context), + ); + } catch (error) { + console.error("Failed to send the super password reset link", error); + throw new TRPCError({ + code: "BAD_GATEWAY", + message: `Could not send the reset link to "${channel.name}". Check the channel or use SSH.`, + }); + } + void sendSuperPasswordAlert( + { type: "reset-requested", channelName: channel.name }, + context, + { excludeNotificationIds: [channel.notificationId] }, + ); +}; + +export const lockSuperSessionOverSsh = async (userId: string) => { + const closed = await closeSuperSession(userId); + if (closed) { + await sendSuperPasswordAlert( + { type: "closed", via: "ssh" }, + { user: { id: userId } }, + ); + } + return closed; +}; + +export const resetSuperPasswordOverSsh = async (userId: string) => { + const existed = !!(await findSuperPassword(userId)); + await removeSuperPassword(userId); + if (existed) { + await sendSuperPasswordAlert( + { type: "reset", via: "ssh" }, + { user: { id: userId } }, + ); + } + return existed; +}; diff --git a/packages/server/src/utils/notifications/super-password.ts b/packages/server/src/utils/notifications/super-password.ts new file mode 100644 index 00000000000..00c4fbc3f63 --- /dev/null +++ b/packages/server/src/utils/notifications/super-password.ts @@ -0,0 +1,302 @@ +import type { + custom, + discord, + email, + gotify, + lark, + mattermost, + notifications, + ntfy, + pushover, + resend, + slack, + teams, + telegram, +} from "@dokploy/server/db/schema"; +import SuperPasswordEmail from "@dokploy/server/emails/emails/super-password"; +import { render } from "@react-email/components"; +import { + sendCustomNotification, + sendDiscordNotification, + sendEmailNotification, + sendGotifyNotification, + sendLarkNotification, + sendMattermostNotification, + sendNtfyNotification, + sendPushoverNotification, + sendResendNotification, + sendSlackNotification, + sendTeamsNotification, + sendTelegramNotification, +} from "./utils"; + +type Provider = + | T["$inferSelect"] + | null + | undefined; + +export type SuperPasswordChannel = Pick< + typeof notifications.$inferSelect, + "notificationId" | "name" | "notificationType" | "organizationId" +> & { + superPassword?: boolean | null; + slack?: Provider; + telegram?: Provider; + discord?: Provider; + email?: Provider; + resend?: Provider; + gotify?: Provider; + ntfy?: Provider; + mattermost?: Provider; + custom?: Provider; + lark?: Provider; + pushover?: Provider; + teams?: Provider; +}; + +export type SuperPasswordAlertMessage = { + event: string; + title: string; + summary: string; + details: { label: string; value: string }[]; + action?: { label: string; url: string }; +}; + +const FOOTER = + "Sent because the 'Super password' toggle is on for this notification channel."; + +const escapeHtml = (value: string) => + value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); + +// HTTP header values (ntfy title and actions) must be Latin-1. +const toHeaderText = (value: string) => + value.replaceAll("–", "-").replace(/[^\x20-\x7e]/g, ""); + +// Telegram rejects inline buttons whose URL points at localhost or a bare IP, +// which would drop the whole alert, so those links go into the text instead. +export const canUseTelegramButton = (url: string) => { + try { + const { protocol, hostname } = new URL(url); + return ( + (protocol === "https:" || protocol === "http:") && + hostname.includes(".") && + !/^[\d.]+$/.test(hostname) && + !hostname.includes(":") + ); + } catch { + return false; + } +}; + +export const formatSuperPasswordAlertText = ( + message: SuperPasswordAlertMessage, +) => + [ + message.title, + message.summary, + "", + ...message.details.map(({ label, value }) => `${label}: ${value}`), + ...(message.action + ? ["", `${message.action.label}: ${message.action.url}`] + : []), + ].join("\n"); + +const formatTelegram = ( + message: SuperPasswordAlertMessage, + withInlineLink: boolean, +) => + [ + `🔐 ${escapeHtml(message.title)}`, + escapeHtml(message.summary), + "", + ...message.details.map( + ({ label, value }) => `${escapeHtml(label)}: ${escapeHtml(value)}`, + ), + ...(message.action && withInlineLink + ? [ + "", + `
${escapeHtml(message.action.label)}`, + ] + : []), + ].join("\n"); + +const formatMarkdown = (message: SuperPasswordAlertMessage) => + [ + `**🔐 ${message.title}**`, + message.summary, + "", + ...message.details.map(({ label, value }) => `**${label}:** ${value}`), + ...(message.action + ? ["", `[${message.action.label}](${message.action.url})`] + : []), + ].join("\n"); + +export const sendSuperPasswordAlertToChannel = async ( + channel: SuperPasswordChannel, + message: SuperPasswordAlertMessage, +) => { + const text = formatSuperPasswordAlertText(message); + + switch (channel.notificationType) { + case "telegram": { + if (!channel.telegram) break; + const button = + message.action && canUseTelegramButton(message.action.url) + ? [[{ text: message.action.label, url: message.action.url }]] + : undefined; + await sendTelegramNotification( + channel.telegram, + formatTelegram(message, !button), + button, + ); + return; + } + case "email": + case "resend": { + const html = await render( + SuperPasswordEmail({ + title: message.title, + intro: message.summary, + details: message.details, + action: message.action, + footer: FOOTER, + }), + ); + if (channel.notificationType === "email" && channel.email) { + await sendEmailNotification(channel.email, message.title, html); + return; + } + if (channel.notificationType === "resend" && channel.resend) { + await sendResendNotification(channel.resend, message.title, html); + return; + } + break; + } + case "discord": { + if (!channel.discord) break; + await sendDiscordNotification(channel.discord, { + title: `🔐 ${message.title}`, + description: message.action + ? `${message.summary}\n\n[${message.action.label}](${message.action.url})` + : message.summary, + color: 0xf59e0b, + fields: message.details.map(({ label, value }) => ({ + name: label, + value: value || "-", + inline: false, + })), + timestamp: new Date().toISOString(), + footer: { text: FOOTER }, + }); + return; + } + case "slack": { + if (!channel.slack) break; + await sendSlackNotification(channel.slack, { + channel: channel.slack.channel, + attachments: [ + { + color: "#f59e0b", + pretext: `:closed_lock_with_key: *${message.title}*`, + text: message.action + ? `${message.summary}\n<${message.action.url}|${message.action.label}>` + : message.summary, + fields: message.details.map(({ label, value }) => ({ + title: label, + value, + short: false, + })), + }, + ], + }); + return; + } + case "mattermost": { + if (!channel.mattermost) break; + await sendMattermostNotification(channel.mattermost, { + text: formatMarkdown(message), + channel: channel.mattermost.channel, + username: channel.mattermost.username || "Dokploy", + }); + return; + } + case "gotify": { + if (!channel.gotify) break; + await sendGotifyNotification(channel.gotify, message.title, text); + return; + } + case "ntfy": { + if (!channel.ntfy) break; + await sendNtfyNotification( + channel.ntfy, + toHeaderText(message.title), + "closed_lock_with_key", + message.action + ? `view, ${toHeaderText(message.action.label)}, ${message.action.url}, clear=true` + : "", + text, + ); + return; + } + case "lark": { + if (!channel.lark) break; + await sendLarkNotification(channel.lark, { + msg_type: "text", + content: { text }, + }); + return; + } + case "pushover": { + if (!channel.pushover) break; + await sendPushoverNotification(channel.pushover, message.title, text); + return; + } + case "teams": { + if (!channel.teams) break; + await sendTeamsNotification(channel.teams, { + title: `🔐 ${message.title}`, + facts: [ + { name: "Event", value: message.summary }, + ...message.details.map(({ label, value }) => ({ + name: label, + value, + })), + ], + ...(message.action && { + potentialAction: { + type: "Action.OpenUrl" as const, + title: message.action.label, + url: message.action.url, + }, + }), + }); + return; + } + case "custom": { + if (!channel.custom) break; + await sendCustomNotification(channel.custom, { + type: "super-password", + event: message.event, + title: message.title, + message: message.summary, + details: Object.fromEntries( + message.details.map(({ label, value }) => [label, value]), + ), + ...(message.action && { + link: message.action.url, + linkLabel: message.action.label, + }), + timestamp: new Date().toISOString(), + }); + return; + } + } + + throw new Error( + `Notification channel "${channel.name}" has no ${channel.notificationType} settings`, + ); +}; diff --git a/packages/server/src/utils/traefik/application.ts b/packages/server/src/utils/traefik/application.ts index 5f870dc1d26..f6c5795b670 100644 --- a/packages/server/src/utils/traefik/application.ts +++ b/packages/server/src/utils/traefik/application.ts @@ -196,9 +196,49 @@ export const readMonitoringConfig = async (readAll = false) => { return null; }; +// ACME storage and uploaded certificates hold TLS private keys, so the generic +// Traefik file editor only opens them when the caller passed the super session +// check (allowProtected). +export const isProtectedTraefikPath = ( + configPath: string, + serverId?: string, +) => { + const { CERTIFICATES_PATH } = paths(!!serverId); + const certificatesPath = path.resolve(CERTIFICATES_PATH); + const resolvedPath = path.resolve(configPath); + const fileName = path.basename(resolvedPath).toLowerCase(); + + return ( + fileName === "acme.json" || + fileName.endsWith(".key") || + resolvedPath === certificatesPath || + resolvedPath.startsWith(`${certificatesPath}${path.sep}`) + ); +}; + +export const filterProtectedTraefikEntries = < + T extends { id: string; children?: T[] }, +>( + entries: T[], + serverId?: string, +): T[] => + entries + .filter((entry) => !isProtectedTraefikPath(entry.id, serverId)) + .map((entry) => + entry.children + ? { + ...entry, + children: filterProtectedTraefikEntries(entry.children, serverId), + } + : entry, + ); + +type TraefikFileAccessOptions = { allowProtected?: boolean }; + export const resolveTraefikConfigPath = ( pathFile: string, serverId?: string, + options: TraefikFileAccessOptions = {}, ) => { if ( typeof pathFile !== "string" || @@ -219,11 +259,19 @@ export const resolveTraefikConfigPath = ( throw new Error("Invalid Traefik config path"); } + if (!options.allowProtected && isProtectedTraefikPath(configPath, serverId)) { + throw new Error("Access to this Traefik file is not allowed"); + } + return configPath; }; -export const readConfigInPath = async (pathFile: string, serverId?: string) => { - const configPath = resolveTraefikConfigPath(pathFile, serverId); +export const readConfigInPath = async ( + pathFile: string, + serverId?: string, + options?: TraefikFileAccessOptions, +) => { + const configPath = resolveTraefikConfigPath(pathFile, serverId, options); if (serverId) { const { stdout } = await execAsyncRemote( @@ -268,8 +316,9 @@ export const writeTraefikConfigInPath = async ( pathFile: string, traefikConfig: string, serverId?: string, + options?: TraefikFileAccessOptions, ) => { - const configPath = resolveTraefikConfigPath(pathFile, serverId); + const configPath = resolveTraefikConfigPath(pathFile, serverId, options); try { if (serverId) {