diff --git a/apps/dokploy/__test__/api/no-session-error.test.ts b/apps/dokploy/__test__/api/no-session-error.test.ts index 22f21f511b6..1809db6a2c1 100644 --- a/apps/dokploy/__test__/api/no-session-error.test.ts +++ b/apps/dokploy/__test__/api/no-session-error.test.ts @@ -29,11 +29,6 @@ vi.mock("@dokploy/server/services/known-secrets", () => ({ resetKnownSecretMatcherCache: () => {}, })); -vi.mock("@/server/api/utils/super-session", async (original) => ({ - ...(await original()), - enforceProcedureSuperSession: async () => {}, -})); - const { adminProcedure, cliProcedure, diff --git a/apps/dokploy/__test__/api/no-session-polling.test.ts b/apps/dokploy/__test__/api/no-session-polling.test.ts index 68c9a044a41..2242b978025 100644 --- a/apps/dokploy/__test__/api/no-session-polling.test.ts +++ b/apps/dokploy/__test__/api/no-session-polling.test.ts @@ -26,11 +26,6 @@ vi.mock("@dokploy/server/services/known-secrets", () => ({ resetKnownSecretMatcherCache: () => {}, })); -vi.mock("@/server/api/utils/super-session", async (original) => ({ - ...(await original()), - enforceProcedureSuperSession: async () => {}, -})); - const { createTRPCContext, createTRPCRouter, diff --git a/apps/dokploy/__test__/docker/docker-router-security.test.ts b/apps/dokploy/__test__/docker/docker-router-security.test.ts index e355ccebb40..94314eb0d5c 100644 --- a/apps/dokploy/__test__/docker/docker-router-security.test.ts +++ b/apps/dokploy/__test__/docker/docker-router-security.test.ts @@ -6,7 +6,6 @@ const mocks = vi.hoisted(() => ({ assertLocalDockerContainerAccess: vi.fn(), assertLocalDockerContainerOrSystemAccess: vi.fn(), checkPermission: vi.fn(), - checkProtectedResourceAccess: vi.fn(), containerKill: vi.fn(), containerRemove: vi.fn(), containerRestart: vi.fn(), @@ -66,17 +65,7 @@ vi.mock("@/server/api/utils/local-docker-access", () => ({ 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 = (authMethod: "session" | "api-key" = "session") => dockerRouter.createCaller({ @@ -292,9 +281,6 @@ describe("docker router container file and event boundary", () => { (ctx, containerId, permission) => mocks.assertLocalDockerContainerAccess(ctx, containerId, permission), ); - mocks.checkProtectedResourceAccess.mockResolvedValue( - "super-session-required", - ); }); const fileCalls = ( @@ -467,28 +453,7 @@ describe("docker router container file and event boundary", () => { ); }); - 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); - + it("opens system container files for owners and admins", async () => { for (const call of fileCalls(createCaller())) { await call.run(); expect(call.sideEffect, call.name).toHaveBeenCalledWith( @@ -501,8 +466,7 @@ describe("docker router container file and event boundary", () => { } }); - it("keeps members out even with an open super session", async () => { - mocks.checkProtectedResourceAccess.mockResolvedValue(null); + it("keeps members out of system container files", async () => { mocks.findMemberByUserId.mockResolvedValue({ role: "member" }); for (const call of fileCalls(createCaller())) { @@ -514,11 +478,6 @@ describe("docker router container file and event boundary", () => { }); 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", diff --git a/apps/dokploy/__test__/docker/docker-volume-router-security.test.ts b/apps/dokploy/__test__/docker/docker-volume-router-security.test.ts index 39386380737..dabf4f82997 100644 --- a/apps/dokploy/__test__/docker/docker-volume-router-security.test.ts +++ b/apps/dokploy/__test__/docker/docker-volume-router-security.test.ts @@ -4,7 +4,6 @@ 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(), @@ -46,18 +45,11 @@ vi.mock("@/server/api/utils/local-docker-access", () => ({ 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 = () => +const createCaller = (authMethod: "session" | "api-key" = "session") => dockerVolumeRouter.createCaller({ db: {}, req: {}, @@ -65,6 +57,7 @@ const createCaller = () => session: { userId: "user-1", activeOrganizationId: "org-1", + ...(authMethod === "api-key" ? { authMethod } : {}), }, user: { id: "user-1", @@ -72,8 +65,11 @@ const createCaller = () => }, } as never); -const volumeCalls = (serverId?: string) => { - const caller = createCaller(); +const volumeCalls = ( + serverId?: string, + authMethod: "session" | "api-key" = "session", +) => { + const caller = createCaller(authMethod); return [ { name: "listVolumeFiles", @@ -133,9 +129,6 @@ describe("docker volume router access boundary", () => { 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 () => { @@ -214,15 +207,17 @@ describe("docker volume router access boundary", () => { }); describe("panel system volumes", () => { - const fileCalls = () => - volumeCalls().filter((call) => call.name !== "removeVolume"); + const fileCalls = (authMethod?: "session" | "api-key") => + volumeCalls(undefined, authMethod).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()) { + it("never opens local system volume files through an API key", async () => { + for (const call of fileCalls("api-key")) { await expect(call.run(), call.name).rejects.toMatchObject({ code: "FORBIDDEN", }); @@ -231,9 +226,7 @@ describe("docker volume router access boundary", () => { expect(mocks.isLocalSystemDockerVolume).toHaveBeenCalledWith("data"); }); - it("opens local system volume files with an open super session", async () => { - mocks.checkProtectedResourceAccess.mockResolvedValue(null); - + it("opens local system volume files for owners and admins", async () => { for (const call of fileCalls()) { await call.run(); expect(call.sideEffect, call.name).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 index aa965e8c9ea..97c7aa253f1 100644 --- a/apps/dokploy/__test__/docker/local-docker-system-access.test.ts +++ b/apps/dokploy/__test__/docker/local-docker-system-access.test.ts @@ -88,7 +88,7 @@ describe("local Docker system containers", () => { mocks.getConfig.mockResolvedValue(container); const assertSystemAccess = vi .fn() - .mockRejectedValue(new Error("super session required")); + .mockRejectedValue(new Error("system access denied")); await expect( assertLocalDockerContainerOrSystemAccess( @@ -97,7 +97,7 @@ describe("local Docker system containers", () => { "write", assertSystemAccess, ), - ).rejects.toThrow("super session required"); + ).rejects.toThrow("system access denied"); expect(assertSystemAccess).toHaveBeenCalledTimes(1); assertSystemAccess.mockResolvedValue(undefined); diff --git a/apps/dokploy/__test__/domain-overview/domain-overview-router.test.ts b/apps/dokploy/__test__/domain-overview/domain-overview-router.test.ts index 806b62eb58d..94ab9f9d53d 100644 --- a/apps/dokploy/__test__/domain-overview/domain-overview-router.test.ts +++ b/apps/dokploy/__test__/domain-overview/domain-overview-router.test.ts @@ -5,8 +5,6 @@ const mocks = vi.hoisted(() => ({ assertNoTraefikDeletion: vi.fn(), clearStaleDeletionLock: vi.fn(), recoverInterruptedTraefik: vi.fn(), - checkProtectedResourceAccess: vi.fn(), - checkSuperSessionAccess: vi.fn(), createAuditLog: vi.fn(), deleteUnusedCertificates: vi.fn(), findMember: vi.fn(), @@ -42,14 +40,6 @@ vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ hasValidLicense: mocks.hasValidLicense, })); -vi.mock("@dokploy/server/services/super-password", async (importOriginal) => ({ - ...(await importOriginal< - typeof import("@dokploy/server/services/super-password") - >()), - checkProtectedResourceAccess: mocks.checkProtectedResourceAccess, - checkSuperSessionAccess: mocks.checkSuperSessionAccess, -})); - vi.mock("@dokploy/server/services/known-secrets", async () => { const { EMPTY_KNOWN_SECRET_MATCHER } = await import( "@dokploy/server/utils/security/secret-scrub" @@ -111,8 +101,6 @@ describe("domain overview router access", () => { { serverId: "server-2" }, ]); mocks.hasValidLicense.mockResolvedValue(false); - mocks.checkProtectedResourceAccess.mockResolvedValue(null); - mocks.checkSuperSessionAccess.mockResolvedValue(null); mocks.getDomainOverview.mockResolvedValue({ rows: [], serverErrors: [], @@ -180,47 +168,24 @@ describe("domain overview router access", () => { await expect(access.canReadHost("server-9")).resolves.toBe(false); }); - it("keeps certificate data closed for a member even with an open super session", async () => { + it("keeps certificate data closed for a member", async () => { await createCaller("member", { traefikFiles: true }).domainOverview.list(); expect(overviewAccess().canReadCertificates).toBe(false); }); - it("keeps certificate data closed while the owner's super session is closed", async () => { - mocks.checkProtectedResourceAccess.mockResolvedValue( - "super-session-required", - ); - - await createCaller("owner").domainOverview.list(); - - expect(overviewAccess().canReadCertificates).toBe(false); - }); - - it("opens certificate data to an owner with an open super session", async () => { + it("opens certificate data to an owner", async () => { await createCaller("owner").domainOverview.list(); expect(overviewAccess().canReadCertificates).toBe(true); - expect(mocks.checkProtectedResourceAccess).toHaveBeenCalledWith({ - userId: "user-1", - viaApiKey: false, - }); }); it("never opens certificate data through an API key", async () => { - mocks.checkProtectedResourceAccess.mockImplementation( - async ({ viaApiKey }: { viaApiKey: boolean }) => - viaApiKey ? "browser-session-required" : null, - ); - await createCaller("owner", { authMethod: "api-key", }).domainOverview.list(); expect(overviewAccess().canReadCertificates).toBe(false); - expect(mocks.checkProtectedResourceAccess).toHaveBeenCalledWith({ - userId: "user-1", - viaApiKey: true, - }); }); it("passes a remote server to the Traefik logs for a member with Traefik file access", async () => { @@ -258,17 +223,6 @@ describe("domain overview router access", () => { expect(mocks.getTraefikLogs).toHaveBeenCalledWith("org-1", null); }); - - it("classifies the certificate deletion as dangerous, like editing a Traefik file", async () => { - const { PROCEDURE_ACCESS } = await import( - "@/server/api/utils/procedure-access" - ); - - expect(PROCEDURE_ACCESS["domainOverview.deleteUnusedCertificates"]).toBe( - "dangerous", - ); - expect(PROCEDURE_ACCESS["settings.updateTraefikFile"]).toBe("dangerous"); - }); }); describe("domain overview certificate deletion access", () => { @@ -281,8 +235,6 @@ describe("domain overview certificate deletion access", () => { { serverId: "server-2" }, ]); mocks.hasValidLicense.mockResolvedValue(false); - mocks.checkProtectedResourceAccess.mockResolvedValue(null); - mocks.checkSuperSessionAccess.mockResolvedValue(null); mocks.deleteUnusedCertificates.mockResolvedValue({ status: "deleted", code: "ok", @@ -308,22 +260,11 @@ describe("domain overview certificate deletion access", () => { expect(mocks.deleteUnusedCertificates).not.toHaveBeenCalled(); }); - it("refuses an admin whose procedure-level super session is not open", async () => { - mocks.checkSuperSessionAccess.mockResolvedValue("super-session-required"); - - await expect( - createCaller("admin").domainOverview.deleteUnusedCertificates(input), - ).rejects.toMatchObject({ code: "FORBIDDEN" }); - expect(mocks.deleteUnusedCertificates).not.toHaveBeenCalled(); - }); - - it("refuses an admin without an open super session", async () => { - mocks.checkProtectedResourceAccess.mockResolvedValue( - "super-session-required", - ); - + it("refuses an API key session", async () => { await expect( - createCaller("admin").domainOverview.deleteUnusedCertificates(input), + createCaller("admin", { + authMethod: "api-key", + }).domainOverview.deleteUnusedCertificates(input), ).rejects.toMatchObject({ code: "FORBIDDEN" }); expect(mocks.deleteUnusedCertificates).not.toHaveBeenCalled(); expect(mocks.createAuditLog).not.toHaveBeenCalled(); @@ -339,7 +280,7 @@ describe("domain overview certificate deletion access", () => { expect(mocks.deleteUnusedCertificates).not.toHaveBeenCalled(); }); - it("deletes for an admin with an open super session and audits who, where, what and the result", async () => { + it("deletes for an admin and audits who, where, what and the result", async () => { mocks.hasValidLicense.mockResolvedValue(true); const result = @@ -461,23 +402,9 @@ describe.each([ vi.clearAllMocks(); mocks.findServers.mockResolvedValue([{ serverId: "server-1" }]); mocks.hasValidLicense.mockResolvedValue(false); - mocks.checkProtectedResourceAccess.mockResolvedValue(null); - mocks.checkSuperSessionAccess.mockResolvedValue(null); mocks[serviceName].mockResolvedValue(result); }); - it("is classified dangerous", async () => { - const { PROCEDURE_ACCESS } = await import( - "@/server/api/utils/procedure-access" - ); - - expect( - PROCEDURE_ACCESS[ - `domainOverview.${procedure}` as keyof typeof PROCEDURE_ACCESS - ], - ).toBe("dangerous"); - }); - it("refuses a member without Traefik file access", async () => { await expect(call(createCaller("member"))).rejects.toMatchObject({ code: "UNAUTHORIZED", @@ -485,27 +412,14 @@ describe.each([ expect(mocks[serviceName]).not.toHaveBeenCalled(); }); - it("refuses an admin without an open super session", async () => { - mocks.checkProtectedResourceAccess.mockResolvedValue( - "super-session-required", - ); - - await expect(call(createCaller("admin"))).rejects.toMatchObject({ - code: "FORBIDDEN", - }); - expect(mocks[serviceName]).not.toHaveBeenCalled(); - }); - it("refuses an API key session", async () => { - mocks.checkSuperSessionAccess.mockResolvedValue("api-key-write-locked"); - await expect( call(createCaller("admin", { authMethod: "api-key" })), ).rejects.toMatchObject({ code: "FORBIDDEN" }); expect(mocks[serviceName]).not.toHaveBeenCalled(); }); - it("runs for an admin with an open super session", async () => { + it("runs for an admin", async () => { await expect(call(createCaller("admin"))).resolves.toMatchObject(result); expect(mocks[serviceName]).toHaveBeenCalledWith("org-1", "server-1"); }); diff --git a/apps/dokploy/__test__/helpers/fake-drizzle-db.ts b/apps/dokploy/__test__/helpers/fake-drizzle-db.ts index bf27d9b59a1..398b944a742 100644 --- a/apps/dokploy/__test__/helpers/fake-drizzle-db.ts +++ b/apps/dokploy/__test__/helpers/fake-drizzle-db.ts @@ -21,8 +21,8 @@ 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), + * In-memory stand-in for the subset of drizzle that services under test use: + * relational findFirst/findMany, insert (with onConflictDoUpdate), * select ... where (optionally for update), update ... returning and delete, * with eq/ne/gt/lt/isNull/isNotNull/and/or conditions evaluated against stored * rows. diff --git a/apps/dokploy/__test__/security/api-key-output-guard.test.ts b/apps/dokploy/__test__/security/api-key-output-guard.test.ts index ffab871c21f..a0bc73dbebd 100644 --- a/apps/dokploy/__test__/security/api-key-output-guard.test.ts +++ b/apps/dokploy/__test__/security/api-key-output-guard.test.ts @@ -17,13 +17,6 @@ vi.mock("@dokploy/server/services/known-secrets", () => ({ resetKnownSecretMatcherCache: mocks.resetKnownSecretMatcherCache, })); -vi.mock("@dokploy/server/services/super-password", async (importOriginal) => ({ - ...(await importOriginal< - typeof import("@dokploy/server/services/super-password") - >()), - checkSuperSessionAccess: vi.fn(async () => null), -})); - const { buildKnownSecretMatcher } = await import( "@dokploy/server/utils/security/secret-scrub" ); @@ -33,9 +26,7 @@ const { createTRPCRouter, protectedProcedure } = await import( const { isProcedureDeniedForApiKey } = await import( "@/server/api/utils/api-key-output-guard" ); -const { PROCEDURE_ACCESS } = await import( - "@/server/api/utils/procedure-access" -); +const { appRouter } = await import("@/server/api/root"); const handled = vi.fn(); @@ -288,9 +279,10 @@ describe("procedures that return raw secrets", () => { }); it("cover every reveal procedure of the app router", () => { - const reveals = Object.keys(PROCEDURE_ACCESS).filter((path) => - /reveal/i.test(path), - ); + const reveals = Object.keys( + (appRouter as unknown as { _def: { procedures: object } })._def + .procedures, + ).filter((path) => /reveal/i.test(path)); expect(reveals.length).toBeGreaterThan(0); for (const path of reveals) { diff --git a/apps/dokploy/__test__/security/bcrypt-hash-compat.test.ts b/apps/dokploy/__test__/security/bcrypt-hash-compat.test.ts index c0ff02121a2..c81d6c54990 100644 --- a/apps/dokploy/__test__/security/bcrypt-hash-compat.test.ts +++ b/apps/dokploy/__test__/security/bcrypt-hash-compat.test.ts @@ -8,11 +8,6 @@ const LEGACY_HASHES = [ password: "legacy-login-password", hash: "$2b$10$OZR9Sl3nuE.yt3N7Uz77Eu./0x/5vtxeregQv82qIwpWTJvrXoMMy", }, - { - label: "super password ($2b$, cost 12)", - password: "legacy-super-password", - hash: "$2b$12$l/i9B3BhLSjMvj0tSNi90.ArVCiYRq4rZSccKNrYh2woqzIiyadES", - }, { label: "$2a$ hash (cost 10)", password: "legacy-basic-auth", diff --git a/apps/dokploy/__test__/security/super-password-alerts.test.ts b/apps/dokploy/__test__/security/super-password-alerts.test.ts deleted file mode 100644 index 07f04fb6218..00000000000 --- a/apps/dokploy/__test__/security/super-password-alerts.test.ts +++ /dev/null @@ -1,590 +0,0 @@ -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 deleted file mode 100644 index 06ab2795bc7..00000000000 --- a/apps/dokploy/__test__/security/super-password-service.test.ts +++ /dev/null @@ -1,575 +0,0 @@ -import bcrypt from "bcrypt"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -// The service hashes and compares with bcrypt at cost 12; the lock tests run -// six of those, ~1.5 s locally and past the 5 s default on a busy CI runner. -vi.setConfig({ testTimeout: 20_000 }); - -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 deleted file mode 100644 index 3fc724a8da0..00000000000 --- a/apps/dokploy/__test__/security/super-password-telegram.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -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(() => { - // "Time left" is floored to minutes, so a real clock ticking between - // building expiresAt and formatting it renders 23 h 59 min. - vi.useFakeTimers({ toFake: ["Date"] }); - vi.setSystemTime(new Date("2026-10-07T12:00:00Z")); - 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.useRealTimers(); - 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 deleted file mode 100644 index 66483edd555..00000000000 --- a/apps/dokploy/__test__/security/super-session-auth-hook.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -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-blind-env.test.ts b/apps/dokploy/__test__/security/super-session-blind-env.test.ts deleted file mode 100644 index ed1042c88e1..00000000000 --- a/apps/dokploy/__test__/security/super-session-blind-env.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { - applications, - environments, - projects, -} from "@dokploy/server/db/schema"; -import { TRPCError } from "@trpc/server"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -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, - applications: schema.applications, - project: schema.projects, - environment: schema.environments, - webServerSettings: schema.webServerSettings, - }); - return { db: fake.db, fake }; -}); - -vi.mock("@dokploy/server/services/permission", () => ({ - checkEnvironmentAccess: vi.fn(), - checkPermission: vi.fn(), - checkProjectAccess: vi.fn(), - checkServicePermissionAndAccess: vi.fn(), - findMemberByUserId: vi.fn(), -})); - -vi.mock("@/server/api/utils/audit", () => ({ audit: vi.fn() })); - -// API key responses are scrubbed of the secrets the panel stores, which are -// read from tables this suite's fake database does not have. -vi.mock("@dokploy/server/services/known-secrets", () => ({ - getKnownSecretMatcher: async () => ({ size: 0, pattern: null, literals: [] }), - resetKnownSecretMatcherCache: () => {}, -})); - -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 { envRouter } = await import("@/server/api/routers/env"); -const { createTRPCRouter } = await import("@/server/api/trpc"); - -const router = createTRPCRouter({ env: envRouter }); - -const createCaller = (authMethod: "api-key" | "session") => - router.createCaller({ - db: {}, - req: {}, - res: {}, - session: { - id: "session-1", - userId: "user-1", - activeOrganizationId: "org-1", - authMethod, - }, - user: { id: "user-1", role: "owner", email: "owner@example.test" }, - } as never).env; - -const target = { scope: "application", id: "app-1" } as const; - -const storedEnv = () => fake.rows(applications)[0]?.env; - -describe("blind env procedures and the super session", () => { - beforeEach(() => { - fake.reset(); - fake.seed(projects, { - projectId: "project-1", - name: "Project One", - organizationId: "org-1", - }); - fake.seed(environments, { - environmentId: "environment-1", - name: "production", - projectId: "project-1", - }); - fake.seed(applications, { - applicationId: "app-1", - name: "App One", - appName: "app-one", - environmentId: "environment-1", - env: "SERVICE_URL=https://app.example.test\n", - }); - }); - - it("keeps API key writes working while no super password is set", async () => { - const result = await createCaller("api-key").applyChanges({ - ...target, - set: { ADDED_NAME: "1" }, - }); - - expect(result.changed).toBe(true); - expect(storedEnv()).toContain("ADDED_NAME=1"); - }); - - it("rejects API key writes while the super session is closed but still lists keys", async () => { - await service.setSuperPassword({ - userId: "user-1", - password: "super-password-1", - }); - - const error = await createCaller("api-key") - .applyChanges({ ...target, generate: { SERVICE_TOKEN: {} } }) - .catch((reason: unknown) => reason); - - expect(error).toBeInstanceOf(TRPCError); - expect((error as TRPCError).code).toBe("FORBIDDEN"); - expect((error as TRPCError).message).toBe( - service.SUPER_SESSION_MESSAGES["api-key-write-locked"], - ); - expect(storedEnv()).toBe("SERVICE_URL=https://app.example.test\n"); - - const keys = await createCaller("api-key").keys(target); - expect(keys.variables).toEqual([ - { name: "SERVICE_URL", secret: false, empty: false }, - ]); - }); - - it("rejects API key dry runs while the super session is closed", async () => { - await service.setSuperPassword({ - userId: "user-1", - password: "super-password-1", - }); - - await expect( - createCaller("api-key").applyChanges({ - ...target, - set: { SERVICE_URL: "https://guess.example.test" }, - dryRun: true, - }), - ).rejects.toMatchObject({ code: "FORBIDDEN" }); - }); - - 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 result = await createCaller("api-key").applyChanges({ - ...target, - set: { ADDED_NAME: "1" }, - }); - - expect(result.changed).toBe(true); - expect(storedEnv()).toContain("ADDED_NAME=1"); - }); - - it("lets a browser session write without an open super session", async () => { - await service.setSuperPassword({ - userId: "user-1", - password: "super-password-1", - }); - - const result = await createCaller("session").applyChanges({ - ...target, - set: { ADDED_NAME: "1" }, - }); - - expect(result.changed).toBe(true); - }); -}); diff --git a/apps/dokploy/__test__/security/super-session-env-upsert.test.ts b/apps/dokploy/__test__/security/super-session-env-upsert.test.ts deleted file mode 100644 index c2ceac32fd3..00000000000 --- a/apps/dokploy/__test__/security/super-session-env-upsert.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -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 deleted file mode 100644 index 1ba696b0318..00000000000 --- a/apps/dokploy/__test__/security/super-session-guard.test.ts +++ /dev/null @@ -1,459 +0,0 @@ -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, -})); - -vi.mock("@dokploy/server/services/known-secrets", async () => { - const { EMPTY_KNOWN_SECRET_MATCHER } = await import( - "@dokploy/server/utils/security/secret-scrub" - ); - return { - getKnownSecretMatcher: async () => EMPTY_KNOWN_SECRET_MATCHER, - resetKnownSecretMatcherCache: () => {}, - }; -}); - -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(browserCaller().application.revealEnvironment()).resolves.toBe( - "application.revealEnvironment", - ); - await expect( - browserCaller().user.update({ email: "new@example.test" }), - ).resolves.toBe("user.update"); - }); - - it("never reveals secrets to API keys, with or without a super password", async () => { - await expect( - apiKeyCaller().application.revealEnvironment(), - ).rejects.toMatchObject({ - code: "FORBIDDEN", - message: - "This procedure returns secret values and is not available to API keys", - }); - expect(handled).not.toHaveBeenCalled(); - }); -}); - -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 deleted file mode 100644 index e89611e629e..00000000000 --- a/apps/dokploy/__test__/security/super-session-procedure-access.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -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.revealApiKey", - "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 54c422d4d94..5e0bf43d171 100644 --- a/apps/dokploy/__test__/settings/docker-server-boundary.test.ts +++ b/apps/dokploy/__test__/settings/docker-server-boundary.test.ts @@ -7,7 +7,6 @@ const mocks = vi.hoisted(() => ({ checkGPUStatus: vi.fn(), checkPermission: vi.fn(), checkPortInUse: vi.fn(), - checkProtectedResourceAccess: vi.fn(), checkPostgresHealth: vi.fn(), checkRedisHealth: vi.fn(), checkTraefikHealth: vi.fn(), @@ -174,12 +173,15 @@ 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/known-secrets", async () => { + const { EMPTY_KNOWN_SECRET_MATCHER } = await import( + "@dokploy/server/utils/security/secret-scrub" + ); + return { + getKnownSecretMatcher: async () => EMPTY_KNOWN_SECRET_MATCHER, + resetKnownSecretMatcherCache: () => {}, + }; +}); vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ hasValidLicense: mocks.hasValidLicense, @@ -220,9 +222,6 @@ vi.mock("../../server/api/root", () => ({ })); const { settingsRouter } = await import("../../server/api/routers/settings"); -const { getSuperSessionDenial } = await import( - "@dokploy/server/services/super-password" -); const createCaller = ( role: "owner" | "admin" | "member" = "admin", @@ -781,9 +780,6 @@ describe("settings Traefik file access", () => { mocks.filterProtectedTraefikEntries.mockImplementation( (entries: unknown[]) => entries, ); - mocks.checkProtectedResourceAccess.mockResolvedValue( - "super-session-required", - ); }); it("requires local host access for local Traefik files", async () => { @@ -815,7 +811,12 @@ describe("settings Traefik file access", () => { expect(mocks.writeTraefikConfigInPath).not.toHaveBeenCalled(); }); - it("hides protected entries from remote Traefik directory listings", async () => { + it("hides protected entries from remote Traefik directory listings of members", async () => { + mocks.assertLocalHostAccess.mockRejectedValue( + Object.assign(new Error("Local host operations require owner or admin"), { + code: "UNAUTHORIZED", + }), + ); const listing = [ { id: `${localTraefikPath}/dynamic/acme.json`, name: "acme.json" }, ]; @@ -823,40 +824,20 @@ describe("settings Traefik file access", () => { mocks.filterProtectedTraefikEntries.mockReturnValue([]); await expect( - createCaller().readDirectories({ serverId: "server-1" }), + createCaller("member").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", () => { + describe("TLS files for owners and admins in a browser", () => { 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); + it("opens TLS files for owners and admins", async () => { const caller = createCaller("owner"); await caller.readTraefikFile({ path: acmePath }); @@ -871,45 +852,62 @@ describe("settings Traefik file access", () => { undefined, { allowProtected: true }, ); - expect(mocks.checkProtectedResourceAccess).toHaveBeenCalledWith({ - userId: "user-1", - viaApiKey: false, - }); + expect(mocks.assertLocalHostAccess).toHaveBeenCalled(); }); - it("never opens TLS files through an API key", async () => { - mocks.checkProtectedResourceAccess.mockImplementation( - async ({ viaApiKey }: { viaApiKey: boolean }) => - viaApiKey ? "browser-session-required" : null, + it("keeps members out of TLS 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.readTraefikFile({ path: acmePath })).rejects.toThrow( + "Local host operations require owner or admin", + ); await expect( - createCaller("owner", "api-key").readTraefikFile({ path: acmePath }), + caller.updateTraefikFile({ path: keyPath, traefikConfig: "x" }), + ).rejects.toThrow("Local host operations require owner or admin"); + expect(mocks.readConfigInPath).not.toHaveBeenCalled(); + expect(mocks.writeTraefikConfigInPath).not.toHaveBeenCalled(); + }); + + it("never opens TLS files through an API key", async () => { + const caller = createCaller("owner", "api-key"); + + await expect( + caller.readTraefikFile({ path: acmePath }), + ).rejects.toMatchObject({ code: "FORBIDDEN" }); + await expect( + caller.updateTraefikFile({ path: keyPath, traefikConfig: "x" }), ).rejects.toMatchObject({ code: "FORBIDDEN" }); expect(mocks.readConfigInPath).not.toHaveBeenCalled(); + expect(mocks.writeTraefikConfigInPath).not.toHaveBeenCalled(); }); - it("does not involve the super session for regular Traefik files", async () => { + it("keeps regular Traefik files open to API keys", async () => { const appPath = `${process.cwd()}/.docker/traefik/dynamic/app.yml`; - await createCaller("owner").readTraefikFile({ path: appPath }); + await createCaller("owner", "api-key").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 () => { + it("lists TLS files only to browser sessions", async () => { const listing = [{ id: acmePath, name: "acme.json" }]; mocks.readDirectory.mockResolvedValue(listing); mocks.filterProtectedTraefikEntries.mockReturnValue([]); - await expect(createCaller("owner").readDirectories({})).resolves.toEqual( - [], - ); + await expect( + createCaller("owner", "api-key").readDirectories({}), + ).resolves.toEqual([]); - mocks.checkProtectedResourceAccess.mockResolvedValue(null); mocks.filterProtectedTraefikEntries.mockClear(); await expect(createCaller("owner").readDirectories({})).resolves.toEqual( listing, diff --git a/apps/dokploy/__test__/setup.ts b/apps/dokploy/__test__/setup.ts index a40f2ccf182..e18c98e7677 100644 --- a/apps/dokploy/__test__/setup.ts +++ b/apps/dokploy/__test__/setup.ts @@ -1,8 +1,8 @@ import { vi } from "vitest"; -// Production hashes use bcrypt cost 12 (~0.25 s each on an idle core), and the -// super password suites hash dozens of times; on a busy CI host that blows the -// 5 s test timeout. Cost 4 keeps the real algorithm and hash format. +// bcrypt is slow by design, and on a busy CI host a suite that hashes many +// times can blow the 5 s test timeout. Cost 4 keeps the real algorithm and hash +// format. vi.mock("bcrypt", async (importOriginal) => { type Bcrypt = typeof import("bcrypt"); // The CJS module also exposes itself as `default` at runtime. diff --git a/apps/dokploy/__test__/traefik/traefik-file-boundary.test.ts b/apps/dokploy/__test__/traefik/traefik-file-boundary.test.ts index 361607234fe..c3cc1c0404a 100644 --- a/apps/dokploy/__test__/traefik/traefik-file-boundary.test.ts +++ b/apps/dokploy/__test__/traefik/traefik-file-boundary.test.ts @@ -116,7 +116,7 @@ describe("Traefik file path boundary", () => { }, ); - it("opens TLS secret files only when the caller passed the super session check", async () => { + it("opens TLS secret files only when the caller passed the protected resource check", async () => { const acmePath = "/etc/dokploy/traefik/dynamic/acme.json"; mocks.execAsyncRemote.mockResolvedValue({ stdout: "{}" }); diff --git a/apps/dokploy/__test__/users/api-key-reveal.test.ts b/apps/dokploy/__test__/users/api-key-reveal.test.ts index 16f55b034fd..5c1110e89e9 100644 --- a/apps/dokploy/__test__/users/api-key-reveal.test.ts +++ b/apps/dokploy/__test__/users/api-key-reveal.test.ts @@ -2,8 +2,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ checkPermission: vi.fn(), - checkProtectedResourceAccess: vi.fn(), - checkSuperSessionAccess: vi.fn(), findApiKeySecret: vi.fn(), apikeyFindFirst: vi.fn(), audit: vi.fn(), @@ -23,15 +21,6 @@ vi.mock("@dokploy/server/db", () => ({ }, })); -vi.mock("@dokploy/server/services/super-password", async (importOriginal) => ({ - ...(await importOriginal< - typeof import("@dokploy/server/services/super-password") - >()), - checkProtectedResourceAccess: mocks.checkProtectedResourceAccess, - // The router-wide super session middleware; covered by its own tests. - checkSuperSessionAccess: mocks.checkSuperSessionAccess, -})); - vi.mock("@dokploy/server/services/permission", () => ({ assertRoleAssignmentAllowed: vi.fn(), checkPermission: mocks.checkPermission, @@ -82,13 +71,11 @@ describe("user.revealApiKey", () => { beforeEach(() => { vi.clearAllMocks(); mocks.checkPermission.mockResolvedValue(undefined); - mocks.checkProtectedResourceAccess.mockResolvedValue(null); - mocks.checkSuperSessionAccess.mockResolvedValue(null); mocks.apikeyFindFirst.mockResolvedValue(storedKey); mocks.findApiKeySecret.mockResolvedValue("ci-deploy_secret-value"); }); - it("returns the stored key inside an open super session and audits it", async () => { + it("returns the stored key to a browser session and audits it", async () => { await expect( callerFor().revealApiKey({ apiKeyId: "key-1" }), ).resolves.toEqual({ key: "ci-deploy_secret-value" }); @@ -98,12 +85,9 @@ describe("user.revealApiKey", () => { ); }); - it("asks for the super password when no super session is open", async () => { - mocks.checkProtectedResourceAccess.mockResolvedValue( - "super-session-required", - ); + it("refuses a session without an id", async () => { await expect( - callerFor().revealApiKey({ apiKeyId: "key-1" }), + callerFor({ id: null }).revealApiKey({ apiKeyId: "key-1" }), ).rejects.toMatchObject({ code: "FORBIDDEN" }); expect(mocks.findApiKeySecret).not.toHaveBeenCalled(); }); diff --git a/apps/dokploy/__test__/utils/session-expiry.test.ts b/apps/dokploy/__test__/utils/session-expiry.test.ts index 0074ba4cc2f..011a466179e 100644 --- a/apps/dokploy/__test__/utils/session-expiry.test.ts +++ b/apps/dokploy/__test__/utils/session-expiry.test.ts @@ -75,7 +75,6 @@ describe("isPublicPath", () => { "/reset-password", "/send-reset-password", "/accept-invitation/abc123", - "/super-password/lock", "/register/", ])("treats %s as a page that needs no session", (path) => { expect(isPublicPath(path)).toBe(true); @@ -88,7 +87,7 @@ describe("isPublicPath", () => { "/swagger", "/registered", "/invitations", - "/super-passwords", + "/accept-invitations", ])("treats %s as a page that needs one", (path) => { expect(isPublicPath(path)).toBe(false); }); diff --git a/apps/dokploy/__test__/wss/docker-container-terminal-hangup.test.ts b/apps/dokploy/__test__/wss/docker-container-terminal-hangup.test.ts index 70e7f0f5fbe..70897454e15 100644 --- a/apps/dokploy/__test__/wss/docker-container-terminal-hangup.test.ts +++ b/apps/dokploy/__test__/wss/docker-container-terminal-hangup.test.ts @@ -40,17 +40,6 @@ vi.mock("@dokploy/server/utils/servers/destination", () => ({ resolveServerDestinationHost: vi.fn(async () => "203.0.113.10"), })); -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, })); diff --git a/apps/dokploy/__test__/wss/terminal-super-session.test.ts b/apps/dokploy/__test__/wss/terminal-super-session.test.ts deleted file mode 100644 index 66ae272bf41..00000000000 --- a/apps/dokploy/__test__/wss/terminal-super-session.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -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/domains/show-domain-overview.tsx b/apps/dokploy/components/dashboard/domains/show-domain-overview.tsx index e89395479b8..8a98ff1ba38 100644 --- a/apps/dokploy/components/dashboard/domains/show-domain-overview.tsx +++ b/apps/dokploy/components/dashboard/domains/show-domain-overview.tsx @@ -135,7 +135,7 @@ const CertificateCell = ({ if (!certificatesVisible) { return ( - Hidden without a super session + Owners and admins only ); } @@ -311,8 +311,7 @@ export const ShowDomainOverview = () => { {data && !data.certificatesVisible && ( Certificate dates are hidden. Traefik keeps them in its - certificate store, which only an owner or admin with an open super - session can read. + certificate store, which only an owner or admin can read. )} {data && data.servers.length > 0 && ( diff --git a/apps/dokploy/components/dashboard/domains/unused-certificates-table.tsx b/apps/dokploy/components/dashboard/domains/unused-certificates-table.tsx index c2bdee5bff5..ca8934673ca 100644 --- a/apps/dokploy/components/dashboard/domains/unused-certificates-table.tsx +++ b/apps/dokploy/components/dashboard/domains/unused-certificates-table.tsx @@ -31,7 +31,7 @@ import { type UnusedCertificate, } from "./shared"; -const HIDDEN_HINT = "Hidden without a super session"; +const HIDDEN_HINT = "Owners and admins only"; const labelOf = (item: UnusedCertificate) => item.main ?? item.names[0] ?? ""; diff --git a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx index 1848453395d..9a894d13988 100644 --- a/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx +++ b/apps/dokploy/components/dashboard/settings/api/add-api-key.tsx @@ -448,7 +448,7 @@ export const AddApiKey = () => { API Key Generated Successfully Copy your API key now. You can reveal it again later from this - page with your super password. + page.
diff --git a/apps/dokploy/components/dashboard/settings/api/reveal-api-key.tsx b/apps/dokploy/components/dashboard/settings/api/reveal-api-key.tsx index b63771b8a3f..268b1b1f535 100644 --- a/apps/dokploy/components/dashboard/settings/api/reveal-api-key.tsx +++ b/apps/dokploy/components/dashboard/settings/api/reveal-api-key.tsx @@ -60,7 +60,7 @@ export const RevealApiKey = ({ apiKeyId, name, revealable }: Props) => { {revealable - ? "Show key (requires the super password)" + ? "Show key" : "This key was created before keys could be revealed"} diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index f5ba8244ae1..093f1666445 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -59,7 +59,6 @@ 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", [ @@ -365,7 +364,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, type: notification.notificationType, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "telegram") { form.reset({ @@ -382,7 +380,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "discord") { form.reset({ @@ -398,7 +395,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "email") { form.reset({ @@ -418,7 +414,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "resend") { form.reset({ @@ -435,7 +430,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "gotify") { form.reset({ @@ -453,7 +447,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "ntfy") { form.reset({ @@ -471,7 +464,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "mattermost") { form.reset({ @@ -488,7 +480,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "lark") { form.reset({ @@ -503,7 +494,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { dockerCleanup: notification.dockerCleanup, volumeBackup: notification.volumeBackup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "teams") { form.reset({ @@ -518,7 +508,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "custom") { form.reset({ @@ -541,7 +530,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { volumeBackup: notification.volumeBackup, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } else if (notification.notificationType === "pushover") { form.reset({ @@ -560,7 +548,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: notification.name, dockerCleanup: notification.dockerCleanup, serverThreshold: notification.serverThreshold, - superPassword: notification.superPassword, }); } } else { @@ -593,7 +580,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { volumeBackup, dockerCleanup, serverThreshold, - superPassword, } = data; let promise: Promise | null = null; if (data.type === "slack") { @@ -611,7 +597,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { slackId: notification?.slackId || "", notificationId: notificationId || "", serverThreshold: serverThreshold, - superPassword: superPassword, }); } else if (data.type === "telegram") { promise = telegramMutation.mutateAsync({ @@ -629,7 +614,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", telegramId: notification?.telegramId || "", serverThreshold: serverThreshold, - superPassword: superPassword, }); } else if (data.type === "discord") { promise = discordMutation.mutateAsync({ @@ -646,7 +630,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", discordId: notification?.discordId || "", serverThreshold: serverThreshold, - superPassword: superPassword, }); } else if (data.type === "email") { promise = emailMutation.mutateAsync({ @@ -667,7 +650,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", emailId: notification?.emailId || "", serverThreshold: serverThreshold, - superPassword: superPassword, }); } else if (data.type === "resend") { promise = resendMutation.mutateAsync({ @@ -685,7 +667,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", resendId: notification?.resendId || "", serverThreshold: serverThreshold, - superPassword: superPassword, }); } else if (data.type === "gotify") { promise = gotifyMutation.mutateAsync({ @@ -702,7 +683,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { dockerCleanup: dockerCleanup, decoration: data.decoration, serverThreshold: serverThreshold, - superPassword: superPassword, notificationId: notificationId || "", gotifyId: notification?.gotifyId || "", }); @@ -721,7 +701,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: data.name, dockerCleanup: dockerCleanup, serverThreshold: serverThreshold, - superPassword: superPassword, notificationId: notificationId || "", ntfyId: notification?.ntfyId || "", }); @@ -741,7 +720,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", mattermostId: notification?.mattermostId || "", serverThreshold: serverThreshold, - superPassword: superPassword, }); } else if (data.type === "lark") { promise = larkMutation.mutateAsync({ @@ -757,7 +735,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", larkId: notification?.larkId || "", serverThreshold: serverThreshold, - superPassword: superPassword, }); } else if (data.type === "teams") { promise = teamsMutation.mutateAsync({ @@ -773,7 +750,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { notificationId: notificationId || "", teamsId: notification?.teamsId || "", serverThreshold: serverThreshold, - superPassword: superPassword, }); } else if (data.type === "custom") { // Convert headers array to object @@ -800,7 +776,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: data.name, dockerCleanup: dockerCleanup, serverThreshold: serverThreshold, - superPassword: superPassword, notificationId: notificationId || "", customId: notification?.customId || "", }); @@ -824,7 +799,6 @@ export const HandleNotifications = ({ notificationId }: Props) => { name: data.name, dockerCleanup: dockerCleanup, serverThreshold: serverThreshold, - superPassword: superPassword, notificationId: notificationId || "", pushoverId: notification?.pushoverId || "", }); @@ -1997,28 +1971,6 @@ 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 deleted file mode 100644 index 3cdd94b9ad3..00000000000 --- a/apps/dokploy/components/shared/super-password-unlock-dialog.tsx +++ /dev/null @@ -1,143 +0,0 @@ -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/0212_remove_super_password.sql b/apps/dokploy/drizzle/0212_remove_super_password.sql new file mode 100644 index 00000000000..a03b2124394 --- /dev/null +++ b/apps/dokploy/drizzle/0212_remove_super_password.sql @@ -0,0 +1,4 @@ +DROP TABLE IF EXISTS "super_password" CASCADE;--> statement-breakpoint +DROP TABLE IF EXISTS "super_password_token" CASCADE;--> statement-breakpoint +DROP TABLE IF EXISTS "super_session" CASCADE;--> statement-breakpoint +ALTER TABLE "notification" DROP COLUMN IF EXISTS "superPassword"; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0212_snapshot.json b/apps/dokploy/drizzle/meta/0212_snapshot.json new file mode 100644 index 00000000000..a7b6226c9e0 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0212_snapshot.json @@ -0,0 +1,10922 @@ +{ + "id": "8fc67ae2-db78-4612-a08e-d7a22196d4e4", + "prevId": "e72185e2-f151-4c54-b66d-ac621c7d33c2", + "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.apikey_secret": { + "name": "apikey_secret", + "schema": "", + "columns": { + "apikey_id": { + "name": "apikey_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_secret_apikey_id_apikey_id_fk": { + "name": "apikey_secret_apikey_id_apikey_id_fk", + "tableFrom": "apikey_secret", + "tableTo": "apikey", + "columnsFrom": [ + "apikey_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.42.0'" + }, + "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 + }, + "customName": { + "name": "customName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "telemetryProviderIds": { + "name": "telemetryProviderIds", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "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.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.telemetryProvider": { + "name": "telemetryProvider", + "schema": "", + "columns": { + "telemetryProviderId": { + "name": "telemetryProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "TelemetryProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "signals": { + "name": "signals", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiSecret": { + "name": "apiSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extraConfig": { + "name": "extraConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "telemetryProvider_organizationId_organization_id_fk": { + "name": "telemetryProvider_organizationId_organization_id_fk", + "tableFrom": "telemetryProvider", + "tableTo": "organization", + "columnsFrom": [ + "organizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 * * *'" + }, + "telemetryProviderIds": { + "name": "telemetryProviderIds", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "vectorAgentOrganizationId": { + "name": "vectorAgentOrganizationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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,\"passwordResetGuide\":null,\"supportEmail\":null,\"logoLightUrl\":null,\"loginLogoLightUrl\":null,\"faviconLightUrl\":null,\"dualModeBranding\":false}'::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": { + "webServerSettings_vectorAgentOrganizationId_organization_id_fk": { + "name": "webServerSettings_vectorAgentOrganizationId_organization_id_fk", + "tableFrom": "webServerSettings", + "tableTo": "organization", + "columnsFrom": [ + "vectorAgentOrganizationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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.TelemetryProviderType": { + "name": "TelemetryProviderType", + "schema": "public", + "values": [ + "loki", + "datadog", + "betterstack", + "elasticsearch", + "splunk_hec", + "aws_cloudwatch", + "prometheus_remote_write", + "new_relic", + "influxdb" + ] + }, + "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 5b0a02cc50c..b949b018ce2 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -1485,6 +1485,13 @@ "when": 1793209780808, "tag": "0211_whitelabeling_config_default", "breakpoints": true + }, + { + "idx": 212, + "version": "7", + "when": 1793387672538, + "tag": "0212_remove_super_password", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/esbuild.config.ts b/apps/dokploy/esbuild.config.ts index 211d8e2c813..f0d765f2610 100644 --- a/apps/dokploy/esbuild.config.ts +++ b/apps/dokploy/esbuild.config.ts @@ -28,8 +28,6 @@ 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 496b7322505..31235c94ea3 100644 --- a/apps/dokploy/lib/auth-client.ts +++ b/apps/dokploy/lib/auth-client.ts @@ -8,26 +8,9 @@ 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 deleted file mode 100644 index 38eaa048305..00000000000 --- a/apps/dokploy/lib/super-session-prompt.ts +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 43b8f5d73f6..00000000000 --- a/apps/dokploy/lock-super-session.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 99728cb597e..3b714e86f77 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -14,8 +14,6 @@ "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 ed1f06ed35c..1451162d52a 100644 --- a/apps/dokploy/pages/_app.tsx +++ b/apps/dokploy/pages/_app.tsx @@ -8,7 +8,6 @@ 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"; @@ -51,7 +50,6 @@ const MyApp = ({ - {getLayout()} diff --git a/apps/dokploy/pages/api/application.env.upsert.ts b/apps/dokploy/pages/api/application.env.upsert.ts index 71697e67d34..8217ff10574 100644 --- a/apps/dokploy/pages/api/application.env.upsert.ts +++ b/apps/dokploy/pages/api/application.env.upsert.ts @@ -7,15 +7,10 @@ import { import { apiUpsertApplicationEnv } from "@dokploy/server/db/schema"; import { resetKnownSecretMatcherCache } from "@dokploy/server/services/known-secrets"; 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"; @@ -72,18 +67,6 @@ 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 d362d5cd939..d04a2ede85e 100644 --- a/apps/dokploy/pages/dashboard/settings/profile.tsx +++ b/apps/dokploy/pages/dashboard/settings/profile.tsx @@ -7,7 +7,6 @@ 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"; @@ -20,7 +19,6 @@ 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 deleted file mode 100644 index c3e896f6d6d..00000000000 --- a/apps/dokploy/pages/dashboard/settings/super-password-reset.tsx +++ /dev/null @@ -1,144 +0,0 @@ -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 deleted file mode 100644 index 625cf60c193..00000000000 --- a/apps/dokploy/pages/super-password/lock.tsx +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index 535be9ec03c..00000000000 --- a/apps/dokploy/reset-super-password.ts +++ /dev/null @@ -1,39 +0,0 @@ -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 7dde3bcdcd6..9cfd8a8b3db 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -53,7 +53,6 @@ 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 { telemetryProviderRouter } from "./routers/telemetry-provider"; @@ -109,7 +108,6 @@ 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/docker-volume.ts b/apps/dokploy/server/api/routers/docker-volume.ts index 6dd94080ac2..1c5043c890c 100644 --- a/apps/dokploy/server/api/routers/docker-volume.ts +++ b/apps/dokploy/server/api/routers/docker-volume.ts @@ -13,7 +13,7 @@ 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 { assertProtectedResourceAccess } from "@/server/api/utils/protected-resource-access"; import { createTRPCRouter, withPermission } from "../trpc"; import { assertDockerServerAccess } from "./docker"; diff --git a/apps/dokploy/server/api/routers/docker.ts b/apps/dokploy/server/api/routers/docker.ts index 98c4e1929d1..753de5980ec 100644 --- a/apps/dokploy/server/api/routers/docker.ts +++ b/apps/dokploy/server/api/routers/docker.ts @@ -32,7 +32,7 @@ import { assertLocalDockerContainerOrSystemAccess, type LocalDockerPermission, } from "@/server/api/utils/local-docker-access"; -import { assertProtectedResourceAccess } from "@/server/api/utils/super-session"; +import { assertProtectedResourceAccess } from "@/server/api/utils/protected-resource-access"; import { uploadFileToContainerSchema } from "@/utils/schema"; import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc"; @@ -98,7 +98,7 @@ const resolveAuthorizedContainerId = async ( }; // The file manager may also open the panel's own containers (dokploy, its -// postgres and redis, traefik), but only with an open super session. +// postgres and redis, traefik), but only for an owner or admin in a browser. const resolveAuthorizedFileContainerId = async ( ctx: Parameters[0], containerId: string, diff --git a/apps/dokploy/server/api/routers/domain-overview.ts b/apps/dokploy/server/api/routers/domain-overview.ts index aeca7479bed..4ec68cb91bc 100644 --- a/apps/dokploy/server/api/routers/domain-overview.ts +++ b/apps/dokploy/server/api/routers/domain-overview.ts @@ -11,7 +11,7 @@ import { audit } from "@/server/api/utils/audit"; import { assertProtectedResourceAccess, hasProtectedResourceAccess, -} from "@/server/api/utils/super-session"; +} from "@/server/api/utils/protected-resource-access"; import { assertTraefikFilesHostAccess, hasTraefikFilesHostAccess, diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index 269b9a076f4..a335b18a8b0 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -41,10 +41,6 @@ import { updateTelegramNotification, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; -import { - findSuperPasswordChannelsInOrganization, - notifySuperPasswordChannelChanges, -} from "@dokploy/server/services/super-password"; import { redactNotificationSecrets, redactNotificationSecretsList, @@ -58,7 +54,6 @@ import { withPermission, } from "@/server/api/trpc"; import { audit } from "@/server/api/utils/audit"; -import { getRequestMeta } from "@/server/api/utils/request-meta"; import { apiCreateCustom, apiCreateDiscord, @@ -113,34 +108,8 @@ 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: watchSuperPasswordChannels("create") + createSlack: withPermission("notification", "create") .input(apiCreateSlack) .mutation(async ({ input, ctx }) => { try { @@ -159,7 +128,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateSlack: watchSuperPasswordChannels("update") + updateSlack: withPermission("notification", "update") .input(apiUpdateSlack) .mutation(async ({ input, ctx }) => { try { @@ -203,7 +172,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createTelegram: watchSuperPasswordChannels("create") + createTelegram: withPermission("notification", "create") .input(apiCreateTelegram) .mutation(async ({ input, ctx }) => { try { @@ -225,7 +194,7 @@ export const notificationRouter = createTRPCRouter({ } }), - updateTelegram: watchSuperPasswordChannels("update") + updateTelegram: withPermission("notification", "update") .input(apiUpdateTelegram) .mutation(async ({ input, ctx }) => { try { @@ -270,7 +239,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createDiscord: watchSuperPasswordChannels("create") + createDiscord: withPermission("notification", "create") .input(apiCreateDiscord) .mutation(async ({ input, ctx }) => { try { @@ -292,7 +261,7 @@ export const notificationRouter = createTRPCRouter({ } }), - updateDiscord: watchSuperPasswordChannels("update") + updateDiscord: withPermission("notification", "update") .input(apiUpdateDiscord) .mutation(async ({ input, ctx }) => { try { @@ -346,7 +315,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createEmail: watchSuperPasswordChannels("create") + createEmail: withPermission("notification", "create") .input(apiCreateEmail) .mutation(async ({ input, ctx }) => { try { @@ -364,7 +333,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateEmail: watchSuperPasswordChannels("update") + updateEmail: withPermission("notification", "update") .input(apiUpdateEmail) .mutation(async ({ input, ctx }) => { try { @@ -416,7 +385,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createResend: watchSuperPasswordChannels("create") + createResend: withPermission("notification", "create") .input(apiCreateResend) .mutation(async ({ input, ctx }) => { try { @@ -434,7 +403,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateResend: watchSuperPasswordChannels("update") + updateResend: withPermission("notification", "update") .input(apiUpdateResend) .mutation(async ({ input, ctx }) => { try { @@ -483,7 +452,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - remove: watchSuperPasswordChannels("delete") + remove: withPermission("notification", "delete") .input(apiFindOneNotification) .mutation(async ({ input, ctx }) => { try { @@ -613,7 +582,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createGotify: watchSuperPasswordChannels("create") + createGotify: withPermission("notification", "create") .input(apiCreateGotify) .mutation(async ({ input, ctx }) => { try { @@ -631,7 +600,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateGotify: watchSuperPasswordChannels("update") + updateGotify: withPermission("notification", "update") .input(apiUpdateGotify) .mutation(async ({ input, ctx }) => { try { @@ -676,7 +645,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createNtfy: watchSuperPasswordChannels("create") + createNtfy: withPermission("notification", "create") .input(apiCreateNtfy) .mutation(async ({ input, ctx }) => { try { @@ -694,7 +663,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateNtfy: watchSuperPasswordChannels("update") + updateNtfy: withPermission("notification", "update") .input(apiUpdateNtfy) .mutation(async ({ input, ctx }) => { try { @@ -744,7 +713,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createMattermost: watchSuperPasswordChannels("create") + createMattermost: withPermission("notification", "create") .input(apiCreateMattermost) .mutation(async ({ input, ctx }) => { try { @@ -765,7 +734,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateMattermost: watchSuperPasswordChannels("update") + updateMattermost: withPermission("notification", "update") .input(apiUpdateMattermost) .mutation(async ({ input, ctx }) => { try { @@ -813,7 +782,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createCustom: watchSuperPasswordChannels("create") + createCustom: withPermission("notification", "create") .input(apiCreateCustom) .mutation(async ({ input, ctx }) => { try { @@ -831,7 +800,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateCustom: watchSuperPasswordChannels("update") + updateCustom: withPermission("notification", "update") .input(apiUpdateCustom) .mutation(async ({ input, ctx }) => { try { @@ -876,7 +845,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createLark: watchSuperPasswordChannels("create") + createLark: withPermission("notification", "create") .input(apiCreateLark) .mutation(async ({ input, ctx }) => { try { @@ -894,7 +863,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateLark: watchSuperPasswordChannels("update") + updateLark: withPermission("notification", "update") .input(apiUpdateLark) .mutation(async ({ input, ctx }) => { try { @@ -940,7 +909,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createTeams: watchSuperPasswordChannels("create") + createTeams: withPermission("notification", "create") .input(apiCreateTeams) .mutation(async ({ input, ctx }) => { try { @@ -958,7 +927,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updateTeams: watchSuperPasswordChannels("update") + updateTeams: withPermission("notification", "update") .input(apiUpdateTeams) .mutation(async ({ input, ctx }) => { try { @@ -1002,7 +971,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - createPushover: watchSuperPasswordChannels("create") + createPushover: withPermission("notification", "create") .input(apiCreatePushover) .mutation(async ({ input, ctx }) => { try { @@ -1023,7 +992,7 @@ export const notificationRouter = createTRPCRouter({ }); } }), - updatePushover: watchSuperPasswordChannels("update") + updatePushover: withPermission("notification", "update") .input(apiUpdatePushover) .mutation(async ({ input, ctx }) => { try { diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index ddd1ac238a5..e7a942d1e4a 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -104,7 +104,7 @@ import { logPanelRestart } from "@/server/api/utils/panel-restart-log"; import { assertProtectedResourceAccess, hasProtectedResourceAccess, -} from "@/server/api/utils/super-session"; +} from "@/server/api/utils/protected-resource-access"; import { assertSettingsServerAccess, assertTraefikFilesHostAccess, diff --git a/apps/dokploy/server/api/routers/super-password.ts b/apps/dokploy/server/api/routers/super-password.ts deleted file mode 100644 index a6a0806281f..00000000000 --- a/apps/dokploy/server/api/routers/super-password.ts +++ /dev/null @@ -1,209 +0,0 @@ -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 cc06770426c..f675e334a28 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -44,23 +44,15 @@ import { resolvePermissions, } from "@dokploy/server/services/permission"; import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key"; -import { - checkProtectedResourceAccess, - 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"; import { and, asc, desc, eq, gt, inArray, ne } from "drizzle-orm"; import { z } from "zod"; import { apiKeyNameSchema } from "@/lib/api-keys"; +import { assertBrowserSession } from "@/server/api/utils/api-key-session"; import { audit } from "@/server/api/utils/audit"; import { assertContainerMetricsServiceAccess } from "@/server/api/utils/monitoring-access"; -import { - assertBrowserSession, - isApiKeySession, - superSessionError, -} from "@/server/api/utils/super-session"; import { deleteAccount } from "@/server/utils/account-deletion"; import { adminProcedure, @@ -545,13 +537,6 @@ 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({ @@ -1050,16 +1035,6 @@ export const userRouter = createTRPCRouter({ .input(z.object({ apiKeyId: z.string() })) .mutation(async ({ input, ctx }) => { assertBrowserSession(ctx); - // Unlike other dangerous procedures this one needs an open super - // session even when no super password is set: the key alone is - // enough to act as the user. - const denial = await checkProtectedResourceAccess({ - userId: ctx.user.id, - viaApiKey: isApiKeySession(ctx.session), - }); - if (denial) { - throw superSessionError(denial); - } await checkPermission(ctx, { api: ["read"] }); const found = await findOwnApiKey(ctx, input.apiKeyId); diff --git a/apps/dokploy/server/api/trpc.ts b/apps/dokploy/server/api/trpc.ts index da4a92d672b..7f573bd566d 100644 --- a/apps/dokploy/server/api/trpc.ts +++ b/apps/dokploy/server/api/trpc.ts @@ -17,7 +17,6 @@ import { resolvePermissions, } 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"; @@ -30,10 +29,7 @@ import { scrubApiKeyError, scrubApiKeyResult, } from "@/server/api/utils/api-key-output-guard"; -import { - enforceProcedureSuperSession, - isApiKeySession, -} from "@/server/api/utils/super-session"; +import { isApiKeySession } from "@/server/api/utils/api-key-session"; type Resource = keyof typeof statements; type ActionOf = (typeof statements)[R][number]; @@ -162,7 +158,6 @@ const t = initTRPC ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, - superSession: getSuperSessionDenial(error), noSession: hasNoSession(error), }, }; @@ -183,20 +178,6 @@ 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(); - }, -); - // An API key is the channel scripts and AI agents use, so nothing it receives // may contain a secret value, whatever role the key's owner has. const apiKeyOutputGuard = t.middleware( @@ -223,7 +204,7 @@ const apiKeyOutputGuard = t.middleware( }, ); -const baseProcedure = t.procedure.use(superSessionGuard).use(apiKeyOutputGuard); +const baseProcedure = t.procedure.use(apiKeyOutputGuard); /** * Public (unauthenticated) procedure diff --git a/apps/dokploy/server/api/utils/api-key-output-guard.ts b/apps/dokploy/server/api/utils/api-key-output-guard.ts index 5fccb147213..aa86c245116 100644 --- a/apps/dokploy/server/api/utils/api-key-output-guard.ts +++ b/apps/dokploy/server/api/utils/api-key-output-guard.ts @@ -18,7 +18,7 @@ import { scrubSecretText, } from "@dokploy/server/utils/security/secret-scrub"; import { TRPCError } from "@trpc/server"; -import { isApiKeySession } from "@/server/api/utils/super-session"; +import { isApiKeySession } from "@/server/api/utils/api-key-session"; // These hand back a stored secret or raw file bytes, which no scrubbing can // make safe (a container file may be nothing but one token), so an API key diff --git a/apps/dokploy/server/api/utils/api-key-session.ts b/apps/dokploy/server/api/utils/api-key-session.ts new file mode 100644 index 00000000000..94c822990f0 --- /dev/null +++ b/apps/dokploy/server/api/utils/api-key-session.ts @@ -0,0 +1,23 @@ +import { TRPCError } from "@trpc/server"; + +type SessionLike = + | { + id?: string | null; + authMethod?: string | null; + } + | null + | undefined; + +// 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 assertBrowserSession = (ctx: { session: SessionLike }) => { + if (isApiKeySession(ctx.session) || !ctx.session?.id) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "This action is only available from a browser session.", + }); + } +}; diff --git a/apps/dokploy/server/api/utils/panel-restart-log.ts b/apps/dokploy/server/api/utils/panel-restart-log.ts index ee92993903b..e308f14f233 100644 --- a/apps/dokploy/server/api/utils/panel-restart-log.ts +++ b/apps/dokploy/server/api/utils/panel-restart-log.ts @@ -1,6 +1,6 @@ import type { IncomingMessage } from "node:http"; +import { isApiKeySession } from "@/server/api/utils/api-key-session"; import { getRequestMeta } from "@/server/api/utils/request-meta"; -import { isApiKeySession } from "@/server/api/utils/super-session"; type PanelRestartRequester = { req?: IncomingMessage; diff --git a/apps/dokploy/server/api/utils/procedure-access.ts b/apps/dokploy/server/api/utils/procedure-access.ts deleted file mode 100644 index 4a7ff84ffb7..00000000000 --- a/apps/dokploy/server/api/utils/procedure-access.ts +++ /dev/null @@ -1,804 +0,0 @@ -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 }, - // A download link hands out the whole dump, secrets included. - "backup.getDownloadUrl": "dangerous", - "backup.listBackupFiles": "read", - "backup.listBackupFilesByBackupId": "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.loadComposeVariables": "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", - "domainOverview.deleteUnusedCertificates": "dangerous", - "domainOverview.clearStaleDeletionLock": "dangerous", - "domainOverview.recoverTraefik": "dangerous", - "domainOverview.list": "read", - "domainOverview.traefikLogs": "read", - "env.applyChanges": "write", - "env.keys": "read", - "env.layout": "read", - "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.getGithubComposeFiles": "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.components": "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.hardenWithLogs": "write", - "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.updateComponentsWithLogs": "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.freeUpdateDiskSpace": "write", - "settings.getDiskUsageHistory": "read", - "settings.getDockerDiskUsage": "read", - "settings.getDokployCloudIps": "read", - "settings.getDokployImageKeepCount": "read", - "settings.getDokployImages": "read", - "settings.getDokployVersion": "read", - "settings.getDokployVersionData": "read", - "settings.getIp": "read", - "settings.getLogCleanupStatus": "read", - "settings.getOpenApiDocument": "read", - "settings.getReleaseTag": "read", - "settings.getServerUpdateStatus": "read", - "settings.getTraefikOverview": "read", - "settings.getTraefikPorts": "read", - "settings.getUpdateData": "write", - "settings.getUpdateDiskSpace": "read", - "settings.getWebServerSettings": "read", - "settings.haveActivateRequests": "read", - "settings.haveTraefikDashboardPortEnabled": "read", - "settings.health": "read", - "settings.hostDockerUpgrade": "read", - "settings.hostStack": "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.updateHostStack": "dangerous", - "settings.updateLogCleanup": "write", - "settings.updateMiddlewareTraefikConfig": "dangerous", - "settings.updateRemoteServersOnly": "write", - "settings.updateServer": "dangerous", - "settings.updateServerIp": "dangerous", - "settings.updateTraefikConfig": "dangerous", - "settings.updateTraefikFile": "dangerous", - "settings.updateTraefik": "dangerous", - "settings.updateTraefikPorts": "dangerous", - "settings.updateWebServerTraefikConfig": "dangerous", - "settings.upgradeHostDocker": "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", - // A provider ships every container log and metric to a third-party - // endpoint, so creating, repointing or deploying one is an export channel. - "telemetryProvider.all": "read", - "telemetryProvider.availableTypes": "read", - "telemetryProvider.create": "dangerous", - "telemetryProvider.deployOnServer": "dangerous", - "telemetryProvider.one": "read", - "telemetryProvider.remove": "dangerous", - "telemetryProvider.removeOnServer": "dangerous", - "telemetryProvider.serverStatus": "read", - "telemetryProvider.testConnection": "write", - "telemetryProvider.testConnectionById": "write", - "telemetryProvider.update": "dangerous", - "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.revealApiKey": "dangerous", - "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/protected-resource-access.ts b/apps/dokploy/server/api/utils/protected-resource-access.ts new file mode 100644 index 00000000000..8e5a5529110 --- /dev/null +++ b/apps/dokploy/server/api/utils/protected-resource-access.ts @@ -0,0 +1,32 @@ +import { TRPCError } from "@trpc/server"; +import { isApiKeySession } from "@/server/api/utils/api-key-session"; +import { assertLocalHostAccess } from "@/server/api/utils/local-host-access"; + +type ProtectedResourceCtx = { + user: { id: string }; + session: { authMethod?: string | null; activeOrganizationId: string }; +}; + +// Traefik TLS files and the panel's own containers and volumes hold private +// keys and the panel database, so only an owner or admin reaches them, and +// never through an API key: that is what scripts and AI agents hold. +export const assertProtectedResourceAccess = async ( + ctx: ProtectedResourceCtx, +) => { + if (isApiKeySession(ctx.session)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "This action is only available from a browser session.", + }); + } + await assertLocalHostAccess(ctx); +}; + +export const hasProtectedResourceAccess = async (ctx: ProtectedResourceCtx) => { + try { + await assertProtectedResourceAccess(ctx); + return true; + } catch { + return false; + } +}; diff --git a/apps/dokploy/server/api/utils/super-session.ts b/apps/dokploy/server/api/utils/super-session.ts deleted file mode 100644 index eb72c69cc73..00000000000 --- a/apps/dokploy/server/api/utils/super-session.ts +++ /dev/null @@ -1,103 +0,0 @@ -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 8e3a1f6920b..62593727a62 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -11,7 +11,6 @@ import { canAccessDockerOverWss } from "./authorize"; import { containerBelongsToService } from "./container-ownership"; import { canAccessDockerTerminalWebSocket } from "./docker-permission"; import { verifyWebSocketOrigin } from "./origin"; -import { rejectTerminalWithoutSuperSession } from "./super-session"; import { getTerminalHangupArgs, isValidContainerId, @@ -107,9 +106,6 @@ export const setupDockerContainerTerminalWebSocketServer = ( return; } - if (await rejectTerminalWithoutSuperSession(ws, user, session)) { - return; - } if (rejectApiKeySession(ws, session)) { return; } diff --git a/apps/dokploy/server/wss/super-session.ts b/apps/dokploy/server/wss/super-session.ts deleted file mode 100644 index 40e4ddb36ae..00000000000 --- a/apps/dokploy/server/wss/super-session.ts +++ /dev/null @@ -1,44 +0,0 @@ -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 00ce8f7ca63..ff347f26c07 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -14,7 +14,6 @@ import { rejectApiKeySession } from "./api-key"; import { canAccessTerminalOverWss } from "./authorize"; import { verifyWebSocketOrigin } from "./origin"; import { canAccessServerTerminalWebSocket } from "./server-permission"; -import { rejectTerminalWithoutSuperSession } from "./super-session"; import { parseResizeMessage, parseTerminalSize, @@ -119,9 +118,6 @@ export const setupTerminalWebSocketServer = ( return; } - if (await rejectTerminalWithoutSuperSession(ws, user, session)) { - return; - } if (rejectApiKeySession(ws, session)) { return; } diff --git a/apps/dokploy/utils/api.ts b/apps/dokploy/utils/api.ts index b5ed14caca3..8a917ec185f 100644 --- a/apps/dokploy/utils/api.ts +++ b/apps/dokploy/utils/api.ts @@ -10,7 +10,6 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import superjson from "superjson"; import type { AppRouter } from "@/server/api/root"; import { noSessionLink, retryUnlessNoSession } from "@/utils/session-expiry"; -import { superSessionLink } from "@/utils/super-session-link"; const getBaseUrl = () => { if (typeof window !== "undefined") return ""; @@ -48,7 +47,6 @@ const links = typeof window !== "undefined" ? [ noSessionLink, - superSessionLink, splitLink({ condition: (op) => op.type === "subscription", true: wsLink({ diff --git a/apps/dokploy/utils/session-expiry.ts b/apps/dokploy/utils/session-expiry.ts index 6cf6513329c..f27399dcf3d 100644 --- a/apps/dokploy/utils/session-expiry.ts +++ b/apps/dokploy/utils/session-expiry.ts @@ -14,7 +14,7 @@ const PUBLIC_PATHS = new Set([ "/reset-password", "/send-reset-password", ]); -const PUBLIC_PREFIXES = ["/accept-invitation", "/super-password"]; +const PUBLIC_PREFIXES = ["/accept-invitation"]; // The server marks only a request that arrived without a valid session. The // same UNAUTHORIZED code also answers a signed-in user whose role is not diff --git a/apps/dokploy/utils/super-session-link.ts b/apps/dokploy/utils/super-session-link.ts deleted file mode 100644 index b8edee1cc44..00000000000 --- a/apps/dokploy/utils/super-session-link.ts +++ /dev/null @@ -1,50 +0,0 @@ -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/account.ts b/packages/server/src/db/schema/account.ts index 8e8eda5a530..5011b354988 100644 --- a/packages/server/src/db/schema/account.ts +++ b/packages/server/src/db/schema/account.ts @@ -294,8 +294,8 @@ export const apikeyRelations = relations(apikey, ({ one }) => ({ })); // better-auth keeps only a hash of each key; this encrypted copy lets the -// owner reveal it again behind an open super session. Kept outside the -// apikey table so better-auth's schema stays untouched. +// owner reveal it again from a browser session. Kept outside the apikey table +// so better-auth's schema stays untouched. export const apikeySecret = pgTable("apikey_secret", { apikeyId: text("apikey_id") .primaryKey() diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts index a8d7eaf1e08..dac9f82e761 100644 --- a/packages/server/src/db/schema/index.ts +++ b/packages/server/src/db/schema/index.ts @@ -41,7 +41,6 @@ export * from "./session"; export * from "./shared"; export * from "./ssh-key"; export * from "./sso"; -export * from "./super-password"; export * from "./tag"; export * from "./telemetry-provider"; export * from "./transfer"; diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts index 805c74453aa..c5cb4a841db 100644 --- a/packages/server/src/db/schema/notification.ts +++ b/packages/server/src/db/schema/notification.ts @@ -41,7 +41,6 @@ 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() @@ -282,8 +281,7 @@ export const apiCreateSlack = notificationsSchema webhookUrl: z.string().min(1), channel: z.string(), }) - .required() - .extend({ superPassword: z.boolean().optional() }); + .required(); export const apiUpdateSlack = apiCreateSlack.partial().extend({ notificationId: z.string().min(1), @@ -314,8 +312,7 @@ export const apiCreateTelegram = notificationsSchema chatId: z.string().min(1), messageThreadId: z.string(), }) - .required() - .extend({ superPassword: z.boolean().optional() }); + .required(); export const apiUpdateTelegram = apiCreateTelegram.partial().extend({ notificationId: z.string().min(1), @@ -346,8 +343,7 @@ export const apiCreateDiscord = notificationsSchema webhookUrl: z.string().min(1), decoration: z.boolean(), }) - .required() - .extend({ superPassword: z.boolean().optional() }); + .required(); export const apiUpdateDiscord = apiCreateDiscord.partial().extend({ notificationId: z.string().min(1), @@ -384,8 +380,7 @@ export const apiCreateEmail = notificationsSchema fromAddress: z.string().min(1), toAddresses: z.array(z.string()).min(1), }) - .required() - .extend({ superPassword: z.boolean().optional() }); + .required(); export const apiUpdateEmail = apiCreateEmail.partial().extend({ notificationId: z.string().min(1), @@ -420,8 +415,7 @@ export const apiCreateResend = notificationsSchema fromAddress: z.string().min(1), toAddresses: z.array(z.string()).min(1), }) - .required() - .extend({ superPassword: z.boolean().optional() }); + .required(); export const apiUpdateResend = apiCreateResend.partial().extend({ notificationId: z.string().min(1), @@ -454,8 +448,7 @@ export const apiCreateGotify = notificationsSchema priority: z.number().min(1), decoration: z.boolean(), }) - .required() - .extend({ superPassword: z.boolean().optional() }); + .required(); export const apiUpdateGotify = apiCreateGotify.partial().extend({ notificationId: z.string().min(1), @@ -492,8 +485,7 @@ export const apiCreateNtfy = notificationsSchema accessToken: z.string().optional(), priority: z.number().min(1), }) - .required() - .extend({ superPassword: z.boolean().optional() }); + .required(); export const apiUpdateNtfy = apiCreateNtfy.partial().extend({ notificationId: z.string().min(1), @@ -521,7 +513,6 @@ export const apiCreateMattermost = notificationsSchema serverThreshold: true, }) .extend({ - superPassword: z.boolean().optional(), webhookUrl: z.string().url(), channel: z.string().optional(), username: z.string().optional(), @@ -574,7 +565,6 @@ export const apiCreateCustom = notificationsSchema serverThreshold: true, }) .extend({ - superPassword: z.boolean().optional(), endpoint: z.string().min(1), headers: z.record(z.string(), z.string()).optional(), }); @@ -606,8 +596,7 @@ export const apiCreateLark = notificationsSchema .extend({ webhookUrl: z.string().min(1), }) - .required() - .extend({ superPassword: z.boolean().optional() }); + .required(); export const apiUpdateLark = apiCreateLark.partial().extend({ notificationId: z.string().min(1), @@ -635,8 +624,7 @@ export const apiCreateTeams = notificationsSchema .extend({ webhookUrl: z.string().min(1), }) - .required() - .extend({ superPassword: z.boolean().optional() }); + .required(); export const apiUpdateTeams = apiCreateTeams.partial().extend({ notificationId: z.string().min(1), @@ -662,7 +650,6 @@ 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), @@ -696,7 +683,6 @@ 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 deleted file mode 100644 index 92e460dd5b4..00000000000 --- a/packages/server/src/db/schema/super-password.ts +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 641937ed373..00000000000 --- a/packages/server/src/emails/emails/super-password.tsx +++ /dev/null @@ -1,93 +0,0 @@ -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 279fc1575df..e0f85f99f4c 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -64,7 +64,6 @@ 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 96b1175fed8..b2ecd813495 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -32,7 +32,6 @@ 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"); @@ -190,7 +189,6 @@ const { handler, api } = betterAuth({ "Email and password sign-in is disabled while SSO is enforced", }); } - await enforceSuperSessionForAuthEndpoint(ctx); }), }, async trustedOrigins(request) { diff --git a/packages/server/src/lib/super-session-auth.ts b/packages/server/src/lib/super-session-auth.ts deleted file mode 100644 index f3e95953f91..00000000000 --- a/packages/server/src/lib/super-session-auth.ts +++ /dev/null @@ -1,93 +0,0 @@ -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 fe3f143151a..f95cf26c6bd 100644 --- a/packages/server/src/services/notification.ts +++ b/packages/server/src/services/notification.ts @@ -120,7 +120,6 @@ export const createSlackNotification = async ( notificationType: "slack", organizationId: organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -153,7 +152,6 @@ export const updateSlackNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -220,7 +218,6 @@ export const createTelegramNotification = async ( notificationType: "telegram", organizationId: organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -253,7 +250,6 @@ export const updateTelegramNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -319,7 +315,6 @@ export const createDiscordNotification = async ( notificationType: "discord", organizationId: organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -352,7 +347,6 @@ export const updateDiscordNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -422,7 +416,6 @@ export const createEmailNotification = async ( notificationType: "email", organizationId: organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -475,7 +468,6 @@ export const updateEmailNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -543,7 +535,6 @@ export const createResendNotification = async ( notificationType: "resend", organizationId: organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -576,7 +567,6 @@ export const updateResendNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -642,7 +632,6 @@ export const createGotifyNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, notificationType: "gotify", organizationId: organizationId, }) @@ -695,7 +684,6 @@ 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)) @@ -761,7 +749,6 @@ export const createNtfyNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, notificationType: "ntfy", organizationId: organizationId, }) @@ -814,7 +801,6 @@ 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)) @@ -881,7 +867,6 @@ export const createCustomNotification = async ( notificationType: "custom", organizationId: organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -934,7 +919,6 @@ export const updateCustomNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -1032,7 +1016,6 @@ export const createLarkNotification = async ( notificationType: "lark", organizationId: organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -1065,7 +1048,6 @@ export const updateLarkNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -1131,7 +1113,6 @@ export const createTeamsNotification = async ( notificationType: "teams", organizationId: organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -1164,7 +1145,6 @@ export const updateTeamsNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -1247,7 +1227,6 @@ export const createMattermostNotification = async ( notificationType: "mattermost", organizationId: organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .returning() .then((value) => value[0]); @@ -1280,7 +1259,6 @@ export const updateMattermostNotification = async ( dockerCleanup: input.dockerCleanup, organizationId: input.organizationId, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, }) .where(eq(notifications.notificationId, input.notificationId)) .returning() @@ -1348,7 +1326,6 @@ export const createPushoverNotification = async ( dokployRestart: input.dokployRestart, dockerCleanup: input.dockerCleanup, serverThreshold: input.serverThreshold, - superPassword: input.superPassword, notificationType: "pushover", organizationId: organizationId, }) @@ -1383,7 +1360,6 @@ 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 deleted file mode 100644 index ad98f195515..00000000000 --- a/packages/server/src/services/super-password.ts +++ /dev/null @@ -1,1073 +0,0 @@ -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 deleted file mode 100644 index 00c4fbc3f63..00000000000 --- a/packages/server/src/utils/notifications/super-password.ts +++ /dev/null @@ -1,302 +0,0 @@ -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 ce8f49b83fe..37765085515 100644 --- a/packages/server/src/utils/traefik/application.ts +++ b/packages/server/src/utils/traefik/application.ts @@ -219,10 +219,10 @@ export const readMonitoringConfig = async (readAll = false) => { type TraefikFileAccessOptions = { allowProtected?: boolean }; // 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). Backups made by operators or Traefik hold the same -// keys, so every path segment is checked, not only the last one: acme.json -// siblings, children of a directory with such a name, and names like +// Traefik file editor only opens them when the caller passed the protected +// resource check (allowProtected). Backups made by operators or Traefik hold +// the same keys, so every path segment is checked, not only the last one: +// acme.json siblings, children of a directory with such a name, and names like // old-acme.json or acme.bak.json. Ordinary config names such as acme-shop.yml // stay editable. const isProtectedSegment = (segment: string) => {