mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge branch 'canary' into feature/rancher-desktop-support
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
// boolean,
|
||||
// } from "drizzle-orm/pg-core";
|
||||
|
||||
// export const users_temp = pgTable("users_temp", {
|
||||
// export const user = pgTable("user", {
|
||||
// id: text("id").primaryKey(),
|
||||
// name: text("name").notNull(),
|
||||
// email: text("email").notNull().unique(),
|
||||
@@ -29,7 +29,7 @@
|
||||
// userAgent: text("user_agent"),
|
||||
// userId: text("user_id")
|
||||
// .notNull()
|
||||
// .references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
// .references(() => user.id, { onDelete: "cascade" }),
|
||||
// activeOrganizationId: text("active_organization_id"),
|
||||
// });
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
// providerId: text("provider_id").notNull(),
|
||||
// userId: text("user_id")
|
||||
// .notNull()
|
||||
// .references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
// .references(() => user.id, { onDelete: "cascade" }),
|
||||
// accessToken: text("access_token"),
|
||||
// refreshToken: text("refresh_token"),
|
||||
// idToken: text("id_token"),
|
||||
|
||||
299
packages/server/auth-schema2.ts
Normal file
299
packages/server/auth-schema2.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
integer,
|
||||
index,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
firstName: text("first_name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
twoFactorEnabled: boolean("two_factor_enabled").default(false),
|
||||
role: text("role"),
|
||||
ownerId: text("owner_id"),
|
||||
allowImpersonation: boolean("allow_impersonation").default(false),
|
||||
lastName: text("last_name").default(""),
|
||||
enableEnterpriseFeatures: boolean("enable_enterprise_features"),
|
||||
isValidEnterpriseLicense: boolean("is_valid_enterprise_license"),
|
||||
});
|
||||
|
||||
export const session = pgTable(
|
||||
"session",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
},
|
||||
(table) => [index("session_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const account = pgTable(
|
||||
"account",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("account_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const verification = pgTable(
|
||||
"verification",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
||||
);
|
||||
|
||||
export const apikey = pgTable(
|
||||
"apikey",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
configId: text("config_id").default("default").notNull(),
|
||||
name: text("name"),
|
||||
start: text("start"),
|
||||
referenceId: text("reference_id").notNull(),
|
||||
prefix: text("prefix"),
|
||||
key: text("key").notNull(),
|
||||
refillInterval: integer("refill_interval"),
|
||||
refillAmount: integer("refill_amount"),
|
||||
lastRefillAt: timestamp("last_refill_at"),
|
||||
enabled: boolean("enabled").default(true),
|
||||
rateLimitEnabled: boolean("rate_limit_enabled").default(true),
|
||||
rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000),
|
||||
rateLimitMax: integer("rate_limit_max").default(10),
|
||||
requestCount: integer("request_count").default(0),
|
||||
remaining: integer("remaining"),
|
||||
lastRequest: timestamp("last_request"),
|
||||
expiresAt: timestamp("expires_at"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
updatedAt: timestamp("updated_at").notNull(),
|
||||
permissions: text("permissions"),
|
||||
metadata: text("metadata"),
|
||||
},
|
||||
(table) => [
|
||||
index("apikey_configId_idx").on(table.configId),
|
||||
index("apikey_referenceId_idx").on(table.referenceId),
|
||||
index("apikey_key_idx").on(table.key),
|
||||
],
|
||||
);
|
||||
|
||||
export const ssoProvider = pgTable("sso_provider", {
|
||||
id: text("id").primaryKey(),
|
||||
issuer: text("issuer").notNull(),
|
||||
oidcConfig: text("oidc_config"),
|
||||
samlConfig: text("saml_config"),
|
||||
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
||||
providerId: text("provider_id").notNull().unique(),
|
||||
organizationId: text("organization_id"),
|
||||
domain: text("domain").notNull(),
|
||||
});
|
||||
|
||||
export const twoFactor = pgTable(
|
||||
"two_factor",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
secret: text("secret").notNull(),
|
||||
backupCodes: text("backup_codes").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => [
|
||||
index("twoFactor_secret_idx").on(table.secret),
|
||||
index("twoFactor_userId_idx").on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
export const organization = pgTable(
|
||||
"organization",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").notNull().unique(),
|
||||
logo: text("logo"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
metadata: text("metadata"),
|
||||
},
|
||||
(table) => [uniqueIndex("organization_slug_uidx").on(table.slug)],
|
||||
);
|
||||
|
||||
export const organizationRole = pgTable(
|
||||
"organization_role",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
role: text("role").notNull(),
|
||||
permission: text("permission").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").$onUpdate(
|
||||
() => /* @__PURE__ */ new Date(),
|
||||
),
|
||||
},
|
||||
(table) => [
|
||||
index("organizationRole_organizationId_idx").on(table.organizationId),
|
||||
index("organizationRole_role_idx").on(table.role),
|
||||
],
|
||||
);
|
||||
|
||||
export const member = pgTable(
|
||||
"member",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
role: text("role").default("member").notNull(),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("member_organizationId_idx").on(table.organizationId),
|
||||
index("member_userId_idx").on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
export const invitation = pgTable(
|
||||
"invitation",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
role: text("role"),
|
||||
status: text("status").default("pending").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
inviterId: text("inviter_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => [
|
||||
index("invitation_organizationId_idx").on(table.organizationId),
|
||||
index("invitation_email_idx").on(table.email),
|
||||
],
|
||||
);
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
ssoProviders: many(ssoProvider),
|
||||
twoFactors: many(twoFactor),
|
||||
members: many(member),
|
||||
invitations: many(invitation),
|
||||
}));
|
||||
|
||||
export const sessionRelations = relations(session, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [ssoProvider.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const twoFactorRelations = relations(twoFactor, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [twoFactor.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const organizationRelations = relations(organization, ({ many }) => ({
|
||||
organizationRoles: many(organizationRole),
|
||||
members: many(member),
|
||||
invitations: many(invitation),
|
||||
}));
|
||||
|
||||
export const organizationRoleRelations = relations(
|
||||
organizationRole,
|
||||
({ one }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [organizationRole.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const memberRelations = relations(member, ({ one }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [member.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [member.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const invitationRelations = relations(invitation, ({ one }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [invitation.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [invitation.inviterId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
@@ -19,54 +19,56 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run switch:prod && rm -rf ./dist && tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
|
||||
"build": "npm run switch:prod && rimraf dist && tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
|
||||
"build:types": "tsc --emitDeclarationOnly --experimenta-dts",
|
||||
"switch:dev": "node scripts/switchToSrc.js",
|
||||
"switch:prod": "node scripts/switchToDist.js",
|
||||
"dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
|
||||
"dev": "rimraf dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
|
||||
"esbuild": "tsx ./esbuild.config.ts && tsc --project tsconfig.server.json --emitDeclarationOnly ",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dbml:generate": "npx tsx src/db/schema/dbml.ts",
|
||||
"generate:drizzle": "pnpm dlx @better-auth/cli generate --output auth-schema2.ts --config src/lib/auth-cli.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.5",
|
||||
"@ai-sdk/azure": "^2.0.16",
|
||||
"@ai-sdk/cohere": "^2.0.4",
|
||||
"@ai-sdk/deepinfra": "^1.0.10",
|
||||
"@ai-sdk/mistral": "^2.0.7",
|
||||
"@ai-sdk/openai": "^2.0.16",
|
||||
"@ai-sdk/openai-compatible": "^1.0.10",
|
||||
"@better-auth/utils": "0.2.4",
|
||||
"@ai-sdk/anthropic": "^3.0.44",
|
||||
"@ai-sdk/azure": "^3.0.30",
|
||||
"@ai-sdk/cohere": "^3.0.21",
|
||||
"@ai-sdk/deepinfra": "^2.0.34",
|
||||
"@ai-sdk/mistral": "^3.0.20",
|
||||
"@ai-sdk/openai": "^3.0.29",
|
||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||
"@better-auth/api-key": "1.5.4",
|
||||
"@better-auth/sso": "1.5.4",
|
||||
"@better-auth/utils": "0.3.1",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
"@octokit/rest": "^20.1.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@react-email/components": "^0.0.21",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@trpc/server": "11.10.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"ai": "^5.0.17",
|
||||
"ai-sdk-ollama": "^0.5.1",
|
||||
"ai": "^6.0.86",
|
||||
"ai-sdk-ollama": "^3.7.0",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "v1.2.8-beta.7",
|
||||
"better-auth": "1.5.4",
|
||||
"better-call": "2.0.2",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-dbml-generator": "0.10.0",
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"hi-base32": "^0.5.1",
|
||||
"yaml": "2.8.1",
|
||||
"lodash": "4.17.21",
|
||||
"micromatch": "4.0.8",
|
||||
"nanoid": "3.3.11",
|
||||
"node-os-utils": "1.3.7",
|
||||
"node-os-utils": "2.0.1",
|
||||
"node-pty": "1.0.0",
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
"octokit": "3.1.2",
|
||||
"otpauth": "^9.4.0",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"postgres": "3.4.4",
|
||||
@@ -74,40 +76,46 @@
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"resend": "^6.0.2",
|
||||
"semver": "7.7.3",
|
||||
"shell-quote": "^1.8.1",
|
||||
"slugify": "^1.6.6",
|
||||
"ssh2": "1.15.0",
|
||||
"toml": "3.0.0",
|
||||
"ws": "8.16.0",
|
||||
"zod": "^3.25.32"
|
||||
"yaml": "2.8.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "1.4.21",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/dockerode": "3.3.23",
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "^18.19.104",
|
||||
"@types/node-os-utils": "1.3.4",
|
||||
"@types/node": "^24.4.0",
|
||||
"@types/node-schedule": "2.1.6",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/ssh2": "1.15.1",
|
||||
"@types/ws": "8.5.10",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"esbuild": "0.20.2",
|
||||
"esbuild-plugin-alias": "0.2.1",
|
||||
"postcss": "^8.5.3",
|
||||
"rimraf": "6.1.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.12.0",
|
||||
"packageManager": "pnpm@10.22.0",
|
||||
"engines": {
|
||||
"node": "^20.16.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
"node": "^24.4.0",
|
||||
"pnpm": ">=10.22.0"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,43 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import Docker from "dockerode";
|
||||
|
||||
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
||||
|
||||
export const DOKPLOY_DOCKER_API_VERSION =
|
||||
process.env.DOKPLOY_DOCKER_API_VERSION;
|
||||
export const DOKPLOY_DOCKER_HOST = process.env.DOKPLOY_DOCKER_HOST;
|
||||
export const DOKPLOY_DOCKER_PORT = process.env.DOKPLOY_DOCKER_PORT
|
||||
? Number(process.env.DOKPLOY_DOCKER_PORT)
|
||||
: undefined;
|
||||
|
||||
export const CLEANUP_CRON_JOB = "50 23 * * *";
|
||||
|
||||
type DockerSocketCandidate = {
|
||||
label: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
// Support for Rancher Desktop and other Docker environments
|
||||
const getDockerConfig = () => {
|
||||
const getDockerConfig = (): Docker => {
|
||||
const versionOption = DOKPLOY_DOCKER_API_VERSION
|
||||
? { version: DOKPLOY_DOCKER_API_VERSION }
|
||||
: {};
|
||||
|
||||
// Explicit remote Docker host configuration
|
||||
if (DOKPLOY_DOCKER_HOST) {
|
||||
console.info(
|
||||
`Using remote Docker host: ${DOKPLOY_DOCKER_HOST}${DOKPLOY_DOCKER_PORT ? `:${DOKPLOY_DOCKER_PORT}` : ""}`,
|
||||
);
|
||||
return new Docker({
|
||||
host: DOKPLOY_DOCKER_HOST,
|
||||
...(DOKPLOY_DOCKER_PORT && { port: DOKPLOY_DOCKER_PORT }),
|
||||
...versionOption,
|
||||
});
|
||||
}
|
||||
|
||||
// Local socket auto-detection (Rancher Desktop, Colima, standard Docker)
|
||||
const dockerSocketCandidates: Array<DockerSocketCandidate> = [];
|
||||
|
||||
if (process.env.DOCKER_HOST) {
|
||||
dockerSocketCandidates.push({
|
||||
label: "DOCKER_HOST environment variable",
|
||||
@@ -37,13 +63,14 @@ const getDockerConfig = () => {
|
||||
console.info(
|
||||
`Using Docker socket (${candidate.label}): ${candidate.path}`,
|
||||
);
|
||||
|
||||
return new Docker({ socketPath: candidate.path });
|
||||
return new Docker({
|
||||
socketPath: candidate.path,
|
||||
...versionOption,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.info(
|
||||
`Docker socket initialization failed for ${candidate.label} (${candidate.path}): ${e instanceof Error ? e.message : "Unknown error"
|
||||
}`,
|
||||
`Docker socket initialization failed for ${candidate.label} (${candidate.path}): ${e instanceof Error ? e.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -51,11 +78,18 @@ const getDockerConfig = () => {
|
||||
console.info(
|
||||
"Using default Docker configuration. You can set the DOCKER_HOST environment variable to specify a custom Docker socket path.",
|
||||
);
|
||||
return new Docker();
|
||||
return new Docker({ ...versionOption });
|
||||
};
|
||||
|
||||
export const docker = getDockerConfig();
|
||||
|
||||
console.log(docker);
|
||||
|
||||
// When not set, use the legacy default so 2FA remains working for users who
|
||||
// enabled it before BETTER_AUTH_SECRET was introduced.
|
||||
export const BETTER_AUTH_SECRET =
|
||||
process.env.BETTER_AUTH_SECRET || "better-auth-secret-123456789";
|
||||
|
||||
export const paths = (isServer = false) => {
|
||||
const BASE_PATH =
|
||||
isServer || process.env.NODE_ENV === "production"
|
||||
@@ -77,5 +111,7 @@ export const paths = (isServer = false) => {
|
||||
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
||||
SCHEDULES_PATH: `${BASE_PATH}/schedules`,
|
||||
VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`,
|
||||
VOLUME_BACKUP_LOCK_PATH: `${BASE_PATH}/volume-backup-lock`,
|
||||
PATCH_REPOS_PATH: `${BASE_PATH}/patch-repos`,
|
||||
};
|
||||
};
|
||||
|
||||
47
packages/server/src/db/constants.ts
Normal file
47
packages/server/src/db/constants.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
export const {
|
||||
DATABASE_URL,
|
||||
POSTGRES_PASSWORD_FILE,
|
||||
POSTGRES_USER = "dokploy",
|
||||
POSTGRES_DB = "dokploy",
|
||||
POSTGRES_HOST = "dokploy-postgres",
|
||||
POSTGRES_PORT = "5432",
|
||||
} = process.env;
|
||||
|
||||
function readSecret(path: string): string {
|
||||
try {
|
||||
return fs.readFileSync(path, "utf8").trim();
|
||||
} catch {
|
||||
throw new Error(`Cannot read secret at ${path}`);
|
||||
}
|
||||
}
|
||||
export let dbUrl: string;
|
||||
if (DATABASE_URL) {
|
||||
// Compatibilidad legacy / overrides
|
||||
dbUrl = DATABASE_URL;
|
||||
} else if (POSTGRES_PASSWORD_FILE) {
|
||||
const password = readSecret(POSTGRES_PASSWORD_FILE);
|
||||
dbUrl = `postgres://${POSTGRES_USER}:${encodeURIComponent(
|
||||
password,
|
||||
)}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`;
|
||||
} else {
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
console.warn(`
|
||||
⚠️ [DEPRECATED DATABASE CONFIG]
|
||||
You are using the legacy hardcoded database credentials.
|
||||
This mode WILL BE REMOVED in a future release.
|
||||
|
||||
Please migrate to Docker Secrets using POSTGRES_PASSWORD_FILE.
|
||||
Please execute this command in your server: curl -sSL https://dokploy.com/security/0.26.6.sh | bash
|
||||
`);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
dbUrl =
|
||||
"postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy";
|
||||
} else {
|
||||
dbUrl =
|
||||
"postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy";
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,40 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import { dbUrl } from "./constants";
|
||||
import * as schema from "./schema";
|
||||
|
||||
declare global {
|
||||
var db: PostgresJsDatabase<typeof schema> | undefined;
|
||||
}
|
||||
export { and, eq };
|
||||
export * from "./schema";
|
||||
|
||||
type Database = PostgresJsDatabase<typeof schema>;
|
||||
|
||||
/**
|
||||
* Evita problemas de redeclaración global en monorepos.
|
||||
* No usamos `declare global`.
|
||||
*/
|
||||
const globalForDb = globalThis as unknown as {
|
||||
db?: Database;
|
||||
};
|
||||
|
||||
let dbConnection: Database;
|
||||
|
||||
export let db: PostgresJsDatabase<typeof schema>;
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
db = drizzle(postgres(process.env.DATABASE_URL!), {
|
||||
// En producción no usamos global cache
|
||||
dbConnection = drizzle(postgres(dbUrl), {
|
||||
schema,
|
||||
});
|
||||
} else {
|
||||
if (!global.db)
|
||||
global.db = drizzle(postgres(process.env.DATABASE_URL!), {
|
||||
// En desarrollo reutilizamos conexión para evitar múltiples conexiones
|
||||
if (!globalForDb.db) {
|
||||
globalForDb.db = drizzle(postgres(dbUrl), {
|
||||
schema,
|
||||
});
|
||||
}
|
||||
|
||||
db = global.db;
|
||||
dbConnection = globalForDb.db;
|
||||
}
|
||||
|
||||
export const db: Database = dbConnection;
|
||||
|
||||
export { dbUrl };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
pgTable,
|
||||
text,
|
||||
@@ -9,7 +10,8 @@ import {
|
||||
import { nanoid } from "nanoid";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import { users_temp } from "./user";
|
||||
import { ssoProvider } from "./sso";
|
||||
import { user } from "./user";
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: text("id")
|
||||
@@ -21,7 +23,7 @@ export const account = pgTable("account", {
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
@@ -39,9 +41,9 @@ export const account = pgTable("account", {
|
||||
});
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(users_temp, {
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [users_temp.id],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -65,19 +67,51 @@ export const organization = pgTable("organization", {
|
||||
metadata: text("metadata"),
|
||||
ownerId: text("owner_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const organizationRole = pgTable(
|
||||
"organization_role",
|
||||
{
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
role: text("role").notNull(),
|
||||
permission: text("permission").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").$onUpdate(() => new Date()),
|
||||
},
|
||||
(table) => [
|
||||
index("organizationRole_organizationId_idx").on(table.organizationId),
|
||||
index("organizationRole_role_idx").on(table.role),
|
||||
],
|
||||
);
|
||||
|
||||
export const organizationRoleRelations = relations(
|
||||
organizationRole,
|
||||
({ one }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [organizationRole.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const organizationRelations = relations(
|
||||
organization,
|
||||
({ one, many }) => ({
|
||||
owner: one(users_temp, {
|
||||
owner: one(user, {
|
||||
fields: [organization.ownerId],
|
||||
references: [users_temp.id],
|
||||
references: [user.id],
|
||||
}),
|
||||
servers: many(server),
|
||||
projects: many(projects),
|
||||
members: many(member),
|
||||
ssoProviders: many(ssoProvider),
|
||||
roles: many(organizationRole),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -90,10 +124,13 @@ export const member = pgTable("member", {
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
role: text("role")
|
||||
.notNull()
|
||||
.$type<"owner" | "member" | "admin" | (string & {})>(),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
teamId: text("team_id"),
|
||||
isDefault: boolean("is_default").notNull().default(false),
|
||||
// Permissions
|
||||
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
||||
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
||||
@@ -108,6 +145,12 @@ export const member = pgTable("member", {
|
||||
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
|
||||
.notNull()
|
||||
.default(false),
|
||||
canDeleteEnvironments: boolean("canDeleteEnvironments")
|
||||
.notNull()
|
||||
.default(false),
|
||||
canCreateEnvironments: boolean("canCreateEnvironments")
|
||||
.notNull()
|
||||
.default(false),
|
||||
accessedProjects: text("accesedProjects")
|
||||
.array()
|
||||
.notNull()
|
||||
@@ -127,9 +170,9 @@ export const memberRelations = relations(member, ({ one }) => ({
|
||||
fields: [member.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
user: one(user, {
|
||||
fields: [member.userId],
|
||||
references: [users_temp.id],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -139,13 +182,14 @@ export const invitation = pgTable("invitation", {
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
role: text("role").$type<"owner" | "member" | "admin">(),
|
||||
role: text("role").$type<"owner" | "member" | "admin" | (string & {})>(),
|
||||
status: text("status").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
inviterId: text("inviter_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
teamId: text("team_id"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const invitationRelations = relations(invitation, ({ one }) => ({
|
||||
@@ -161,7 +205,7 @@ export const twoFactor = pgTable("two_factor", {
|
||||
backupCodes: text("backup_codes").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const apikey = pgTable("apikey", {
|
||||
@@ -170,9 +214,10 @@ export const apikey = pgTable("apikey", {
|
||||
start: text("start"),
|
||||
prefix: text("prefix"),
|
||||
key: text("key").notNull(),
|
||||
userId: text("user_id")
|
||||
configId: text("config_id").default("default").notNull(),
|
||||
referenceId: text("reference_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
refillInterval: integer("refill_interval"),
|
||||
refillAmount: integer("refill_amount"),
|
||||
lastRefillAt: timestamp("last_refill_at"),
|
||||
@@ -191,8 +236,8 @@ export const apikey = pgTable("apikey", {
|
||||
});
|
||||
|
||||
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
||||
user: one(users_temp, {
|
||||
fields: [apikey.userId],
|
||||
references: [users_temp.id],
|
||||
user: one(user, {
|
||||
fields: [apikey.referenceId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
integer,
|
||||
json,
|
||||
@@ -18,9 +19,9 @@ import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
import { patch } from "./patch";
|
||||
import { ports } from "./port";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
import { projects } from "./project";
|
||||
import { redirects } from "./redirects";
|
||||
import { registry } from "./registry";
|
||||
import { security } from "./security";
|
||||
@@ -28,6 +29,8 @@ import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
certificateType,
|
||||
type EndpointSpecSwarm,
|
||||
EndpointSpecSwarmSchema,
|
||||
type HealthCheckSwarm,
|
||||
HealthCheckSwarmSchema,
|
||||
type LabelsSwarm,
|
||||
@@ -41,11 +44,13 @@ import {
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
triggerType,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
export const sourceType = pgEnum("sourceType", [
|
||||
"docker",
|
||||
"git",
|
||||
@@ -80,6 +85,7 @@ export const applications = pgTable("application", {
|
||||
previewEnv: text("previewEnv"),
|
||||
watchPaths: text("watchPaths").array(),
|
||||
previewBuildArgs: text("previewBuildArgs"),
|
||||
previewBuildSecrets: text("previewBuildSecrets"),
|
||||
previewLabels: text("previewLabels").array(),
|
||||
previewWildcard: text("previewWildcard"),
|
||||
previewPort: integer("previewPort").default(3000),
|
||||
@@ -99,6 +105,7 @@ export const applications = pgTable("application", {
|
||||
).default(true),
|
||||
rollbackActive: boolean("rollbackActive").default(false),
|
||||
buildArgs: text("buildArgs"),
|
||||
buildSecrets: text("buildSecrets"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
@@ -107,6 +114,7 @@ export const applications = pgTable("application", {
|
||||
enabled: boolean("enabled"),
|
||||
subtitle: text("subtitle"),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
||||
sourceType: sourceType("sourceType").notNull().default("github"),
|
||||
cleanCache: boolean("cleanCache").default(false),
|
||||
@@ -131,6 +139,7 @@ export const applications = pgTable("application", {
|
||||
giteaBuildPath: text("giteaBuildPath").default("/"),
|
||||
// Bitbucket
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketRepositorySlug: text("bitbucketRepositorySlug"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
bitbucketBranch: text("bitbucketBranch"),
|
||||
bitbucketBuildPath: text("bitbucketBuildPath").default("/"),
|
||||
@@ -150,7 +159,7 @@ export const applications = pgTable("application", {
|
||||
},
|
||||
),
|
||||
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
|
||||
dockerfile: text("dockerfile"),
|
||||
dockerfile: text("dockerfile").default("Dockerfile"),
|
||||
dockerContextPath: text("dockerContextPath"),
|
||||
dockerBuildStage: text("dockerBuildStage"),
|
||||
// Drop
|
||||
@@ -164,22 +173,32 @@ export const applications = pgTable("application", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
//
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
buildType: buildType("buildType").notNull().default("nixpacks"),
|
||||
railpackVersion: text("railpackVersion").default("0.2.2"),
|
||||
railpackVersion: text("railpackVersion").default("0.15.4"),
|
||||
herokuVersion: text("herokuVersion").default("24"),
|
||||
publishDirectory: text("publishDirectory"),
|
||||
isStaticSpa: boolean("isStaticSpa"),
|
||||
createEnvFile: boolean("createEnvFile").notNull().default(true),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
registryId: text("registryId").references(() => registry.registryId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
rollbackRegistryId: text("rollbackRegistryId").references(
|
||||
() => registry.registryId,
|
||||
{
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
environmentId: text("environmentId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
@@ -198,6 +217,15 @@ export const applications = pgTable("application", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
buildServerId: text("buildServerId").references(() => server.serverId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
buildRegistryId: text("buildRegistryId").references(
|
||||
() => registry.registryId,
|
||||
{
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const applicationsRelations = relations(
|
||||
@@ -220,6 +248,7 @@ export const applicationsRelations = relations(
|
||||
registry: one(registry, {
|
||||
fields: [applications.registryId],
|
||||
references: [registry.registryId],
|
||||
relationName: "applicationRegistry",
|
||||
}),
|
||||
github: one(github, {
|
||||
fields: [applications.githubId],
|
||||
@@ -240,18 +269,41 @@ export const applicationsRelations = relations(
|
||||
server: one(server, {
|
||||
fields: [applications.serverId],
|
||||
references: [server.serverId],
|
||||
relationName: "applicationServer",
|
||||
}),
|
||||
buildServer: one(server, {
|
||||
fields: [applications.buildServerId],
|
||||
references: [server.serverId],
|
||||
relationName: "applicationBuildServer",
|
||||
}),
|
||||
buildRegistry: one(registry, {
|
||||
fields: [applications.buildRegistryId],
|
||||
references: [registry.registryId],
|
||||
relationName: "applicationBuildRegistry",
|
||||
}),
|
||||
previewDeployments: many(previewDeployments),
|
||||
rollbackRegistry: one(registry, {
|
||||
fields: [applications.rollbackRegistryId],
|
||||
references: [registry.registryId],
|
||||
relationName: "applicationRollbackRegistry",
|
||||
}),
|
||||
patches: many(patch),
|
||||
}),
|
||||
);
|
||||
|
||||
const createSchema = createInsertSchema(applications, {
|
||||
appName: z.string(),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(63)
|
||||
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
|
||||
.optional(),
|
||||
createdAt: z.string(),
|
||||
applicationId: z.string(),
|
||||
autoDeploy: z.boolean(),
|
||||
env: z.string().optional(),
|
||||
buildArgs: z.string().optional(),
|
||||
buildSecrets: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
@@ -265,6 +317,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
username: z.string().optional(),
|
||||
isPreviewDeploymentsActive: z.boolean().optional(),
|
||||
password: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
registryUrl: z.string().optional(),
|
||||
customGitSSHKeyId: z.string().optional(),
|
||||
repository: z.string().optional(),
|
||||
@@ -278,6 +331,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
sourceType: z
|
||||
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
|
||||
.optional(),
|
||||
triggerType: z.enum(["push", "tag"]).optional(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
buildType: z.enum([
|
||||
"dockerfile",
|
||||
@@ -291,6 +345,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
herokuVersion: z.string().optional(),
|
||||
publishDirectory: z.string().optional(),
|
||||
isStaticSpa: z.boolean().optional(),
|
||||
createEnvFile: z.boolean().optional(),
|
||||
owner: z.string(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
@@ -303,15 +358,20 @@ const createSchema = createInsertSchema(applications, {
|
||||
previewPort: z.number().optional(),
|
||||
previewEnv: z.string().optional(),
|
||||
previewBuildArgs: z.string().optional(),
|
||||
previewBuildSecrets: z.string().optional(),
|
||||
previewWildcard: z.string().optional(),
|
||||
previewLimit: z.number().optional(),
|
||||
previewHttps: z.boolean().optional(),
|
||||
previewPath: z.string().optional(),
|
||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
watchPaths: z.array(z.string()).optional().optional(),
|
||||
previewLabels: z.array(z.string()).optional(),
|
||||
cleanCache: z.boolean().optional(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
enableSubmodules: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
@@ -322,11 +382,9 @@ export const apiCreateApplication = createSchema.pick({
|
||||
serverId: true,
|
||||
});
|
||||
|
||||
export const apiFindOneApplication = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneApplication = z.object({
|
||||
applicationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiDeployApplication = createSchema
|
||||
.pick({
|
||||
@@ -376,13 +434,13 @@ export const apiSaveGithubProvider = createSchema
|
||||
owner: true,
|
||||
buildPath: true,
|
||||
githubId: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
triggerType: z.enum(["push", "tag"]).default("push"),
|
||||
});
|
||||
})
|
||||
.required()
|
||||
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
|
||||
|
||||
export const apiSaveGitlabProvider = createSchema
|
||||
.pick({
|
||||
@@ -394,10 +452,9 @@ export const apiSaveGitlabProvider = createSchema
|
||||
gitlabId: true,
|
||||
gitlabProjectId: true,
|
||||
gitlabPathNamespace: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
|
||||
|
||||
export const apiSaveBitbucketProvider = createSchema
|
||||
.pick({
|
||||
@@ -405,12 +462,12 @@ export const apiSaveBitbucketProvider = createSchema
|
||||
bitbucketBuildPath: true,
|
||||
bitbucketOwner: true,
|
||||
bitbucketRepository: true,
|
||||
bitbucketRepositorySlug: true,
|
||||
bitbucketId: true,
|
||||
applicationId: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
|
||||
|
||||
export const apiSaveGiteaProvider = createSchema
|
||||
.pick({
|
||||
@@ -420,10 +477,9 @@ export const apiSaveGiteaProvider = createSchema
|
||||
giteaOwner: true,
|
||||
giteaRepository: true,
|
||||
giteaId: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
|
||||
|
||||
export const apiSaveDockerProvider = createSchema
|
||||
.pick({
|
||||
@@ -448,6 +504,7 @@ export const apiSaveGitProvider = createSchema
|
||||
.merge(
|
||||
createSchema.pick({
|
||||
customGitSSHKeyId: true,
|
||||
enableSubmodules: true,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -456,14 +513,14 @@ export const apiSaveEnvironmentVariables = createSchema
|
||||
applicationId: true,
|
||||
env: true,
|
||||
buildArgs: true,
|
||||
buildSecrets: true,
|
||||
createEnvFile: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindMonitoringStats = createSchema
|
||||
.pick({
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindMonitoringStats = z.object({
|
||||
appName: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateApplication = createSchema
|
||||
.partial()
|
||||
|
||||
94
packages/server/src/db/schema/audit-log.ts
Normal file
94
packages/server/src/db/schema/audit-log.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { index, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { organization } from "./account";
|
||||
import { user } from "./user";
|
||||
|
||||
export const auditLog = pgTable(
|
||||
"audit_log",
|
||||
{
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
organizationId: text("organization_id").references(() => organization.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
userId: text("user_id").references(() => user.id, { onDelete: "set null" }),
|
||||
userEmail: text("user_email").notNull(),
|
||||
userRole: text("user_role").notNull(),
|
||||
action: text("action").notNull(),
|
||||
resourceType: text("resource_type").notNull(),
|
||||
resourceId: text("resource_id"),
|
||||
resourceName: text("resource_name"),
|
||||
metadata: text("metadata"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
orgIdx: index("auditLog_organizationId_idx").on(t.organizationId),
|
||||
userIdx: index("auditLog_userId_idx").on(t.userId),
|
||||
createdAtIdx: index("auditLog_createdAt_idx").on(t.createdAt),
|
||||
}),
|
||||
);
|
||||
|
||||
export const auditLogRelations = relations(auditLog, ({ one }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [auditLog.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [auditLog.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export type AuditLog = typeof auditLog.$inferSelect;
|
||||
export type NewAuditLog = typeof auditLog.$inferInsert;
|
||||
|
||||
export type AuditAction =
|
||||
| "create"
|
||||
| "update"
|
||||
| "delete"
|
||||
| "deploy"
|
||||
| "cancel"
|
||||
| "redeploy"
|
||||
| "login"
|
||||
| "logout"
|
||||
| "restore"
|
||||
| "run"
|
||||
| "start"
|
||||
| "stop"
|
||||
| "reload"
|
||||
| "rebuild"
|
||||
| "move";
|
||||
|
||||
export type AuditResourceType =
|
||||
| "project"
|
||||
| "service"
|
||||
| "environment"
|
||||
| "deployment"
|
||||
| "user"
|
||||
| "customRole"
|
||||
| "domain"
|
||||
| "certificate"
|
||||
| "registry"
|
||||
| "server"
|
||||
| "sshKey"
|
||||
| "gitProvider"
|
||||
| "destination"
|
||||
| "notification"
|
||||
| "settings"
|
||||
| "session"
|
||||
| "port"
|
||||
| "redirect"
|
||||
| "security"
|
||||
| "schedule"
|
||||
| "backup"
|
||||
| "volumeBackup"
|
||||
| "docker"
|
||||
| "swarm"
|
||||
| "previewDeployment"
|
||||
| "organization"
|
||||
| "cluster"
|
||||
| "mount"
|
||||
| "application"
|
||||
| "compose";
|
||||
@@ -15,17 +15,19 @@ import { generateAppName } from ".";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { users_temp } from "./user";
|
||||
import { user } from "./user";
|
||||
export const databaseType = pgEnum("databaseType", [
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]);
|
||||
|
||||
export const backupType = pgEnum("backupType", ["database", "compose"]);
|
||||
@@ -74,7 +76,10 @@ export const backups = pgTable("backup", {
|
||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => users_temp.id),
|
||||
libsqlId: text("libsqlId").references((): AnyPgColumn => libsql.libsqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => user.id),
|
||||
// Only for compose backups
|
||||
metadata: jsonb("metadata").$type<
|
||||
| {
|
||||
@@ -118,9 +123,13 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
|
||||
fields: [backups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
libsql: one(libsql, {
|
||||
fields: [backups.libsqlId],
|
||||
references: [libsql.libsqlId],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [backups.userId],
|
||||
references: [users_temp.id],
|
||||
references: [user.id],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [backups.composeId],
|
||||
@@ -137,11 +146,19 @@ const createSchema = createInsertSchema(backups, {
|
||||
database: z.string().min(1),
|
||||
schedule: z.string(),
|
||||
keepLatestCount: z.number().optional(),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
|
||||
databaseType: z.enum([
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]),
|
||||
postgresId: z.string().optional(),
|
||||
mariadbId: z.string().optional(),
|
||||
mysqlId: z.string().optional(),
|
||||
mongoId: z.string().optional(),
|
||||
libsqlId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
metadata: z.any().optional(),
|
||||
});
|
||||
@@ -157,6 +174,7 @@ export const apiCreateBackup = createSchema.pick({
|
||||
mysqlId: true,
|
||||
postgresId: true,
|
||||
mongoId: true,
|
||||
libsqlId: true,
|
||||
databaseType: true,
|
||||
userId: true,
|
||||
backupType: true,
|
||||
@@ -165,11 +183,9 @@ export const apiCreateBackup = createSchema.pick({
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export const apiFindOneBackup = createSchema
|
||||
.pick({
|
||||
backupId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneBackup = z.object({
|
||||
backupId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiRemoveBackup = createSchema
|
||||
.pick({
|
||||
@@ -194,7 +210,14 @@ export const apiUpdateBackup = createSchema
|
||||
|
||||
export const apiRestoreBackup = z.object({
|
||||
databaseId: z.string(),
|
||||
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
|
||||
databaseType: z.enum([
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]),
|
||||
backupType: z.enum(["database", "compose"]),
|
||||
databaseName: z.string().min(1),
|
||||
backupFile: z.string().min(1),
|
||||
|
||||
@@ -12,12 +12,12 @@ import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
import { projects } from "./project";
|
||||
import { patch } from "./patch";
|
||||
import { schedules } from "./schedule";
|
||||
import { server } from "./server";
|
||||
import { applicationStatus, triggerType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
"git",
|
||||
"github",
|
||||
@@ -57,6 +57,7 @@ export const compose = pgTable("compose", {
|
||||
gitlabPathNamespace: text("gitlabPathNamespace"),
|
||||
// Bitbucket
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketRepositorySlug: text("bitbucketRepositorySlug"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
bitbucketBranch: text("bitbucketBranch"),
|
||||
// Gitea
|
||||
@@ -143,10 +144,17 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
}),
|
||||
backups: many(backups),
|
||||
schedules: many(schedules),
|
||||
patches: many(patch),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(compose, {
|
||||
name: z.string().min(1),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(63)
|
||||
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
|
||||
.optional(),
|
||||
description: z.string(),
|
||||
env: z.string().optional(),
|
||||
composeFile: z.string().optional(),
|
||||
@@ -156,6 +164,11 @@ const createSchema = createInsertSchema(compose, {
|
||||
composePath: z.string().min(1),
|
||||
composeType: z.enum(["docker-compose", "stack"]).optional(),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
sourceType: z
|
||||
.enum(["git", "github", "gitlab", "bitbucket", "gitea", "raw"])
|
||||
.optional(),
|
||||
triggerType: z.enum(["push", "tag"]).optional(),
|
||||
composeStatus: z.enum(["idle", "running", "done", "error"]).optional(),
|
||||
});
|
||||
|
||||
export const apiCreateCompose = createSchema.pick({
|
||||
|
||||
@@ -21,6 +21,7 @@ export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
"done",
|
||||
"error",
|
||||
"cancelled",
|
||||
]);
|
||||
|
||||
export const deployments = pgTable("deployment", {
|
||||
@@ -69,6 +70,9 @@ export const deployments = pgTable("deployment", {
|
||||
(): AnyPgColumn => volumeBackups.volumeBackupId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
buildServerId: text("buildServerId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
@@ -83,6 +87,12 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
server: one(server, {
|
||||
fields: [deployments.serverId],
|
||||
references: [server.serverId],
|
||||
relationName: "deploymentServer",
|
||||
}),
|
||||
buildServer: one(server, {
|
||||
fields: [deployments.buildServerId],
|
||||
references: [server.serverId],
|
||||
relationName: "deploymentBuildServer",
|
||||
}),
|
||||
previewDeployment: one(previewDeployments, {
|
||||
fields: [deployments.previewDeploymentId],
|
||||
@@ -114,8 +124,8 @@ const schema = createInsertSchema(deployments, {
|
||||
composeId: z.string(),
|
||||
description: z.string().optional(),
|
||||
previewDeploymentId: z.string(),
|
||||
buildServerId: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreateDeployment = schema
|
||||
.pick({
|
||||
title: true,
|
||||
@@ -199,44 +209,27 @@ export const apiCreateDeploymentVolumeBackup = schema
|
||||
volumeBackupId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindAllByApplication = schema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
})
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
export const apiFindAllByApplication = z.object({
|
||||
applicationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindAllByCompose = schema
|
||||
.pick({
|
||||
composeId: true,
|
||||
})
|
||||
.extend({
|
||||
composeId: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
export const apiFindAllByCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindAllByServer = schema
|
||||
.pick({
|
||||
serverId: true,
|
||||
})
|
||||
.extend({
|
||||
serverId: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
export const apiFindAllByServer = z.object({
|
||||
serverId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindAllByType = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
type: z.enum([
|
||||
"application",
|
||||
"compose",
|
||||
"server",
|
||||
"schedule",
|
||||
"previewDeployment",
|
||||
"backup",
|
||||
"volumeBackup",
|
||||
]),
|
||||
})
|
||||
.required();
|
||||
export const apiFindAllByType = z.object({
|
||||
id: z.string().min(1),
|
||||
type: z.enum([
|
||||
"application",
|
||||
"compose",
|
||||
"server",
|
||||
"schedule",
|
||||
"previewDeployment",
|
||||
"backup",
|
||||
"volumeBackup",
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -61,11 +61,9 @@ export const apiCreateDestination = createSchema
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneDestination = createSchema
|
||||
.pick({
|
||||
destinationId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneDestination = z.object({
|
||||
destinationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiRemoveDestination = createSchema
|
||||
.pick({
|
||||
|
||||
@@ -70,7 +70,11 @@ export const domainsRelations = relations(domains, ({ one }) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(domains, domain._def.schema.shape);
|
||||
const createSchema = createInsertSchema(domains, {
|
||||
...domain.shape,
|
||||
// Override pgEnum so Zod 4 infers only string literals, not numeric enum index
|
||||
domainType: z.enum(["compose", "application", "preview"]).optional(),
|
||||
});
|
||||
|
||||
export const apiCreateDomain = createSchema.pick({
|
||||
host: true,
|
||||
@@ -88,11 +92,9 @@ export const apiCreateDomain = createSchema.pick({
|
||||
stripPath: true,
|
||||
});
|
||||
|
||||
export const apiFindDomain = createSchema
|
||||
.pick({
|
||||
domainId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindDomain = z.object({
|
||||
domainId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindDomainByApplication = createSchema.pick({
|
||||
applicationId: true,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -26,6 +26,7 @@ export const environments = pgTable("environment", {
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
isDefault: boolean("isDefault").notNull().default(false),
|
||||
});
|
||||
|
||||
export const environmentRelations = relations(
|
||||
@@ -36,50 +37,40 @@ export const environmentRelations = relations(
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
applications: many(applications),
|
||||
mariadb: many(mariadb),
|
||||
postgres: many(postgres),
|
||||
mysql: many(mysql),
|
||||
redis: many(redis),
|
||||
mongo: many(mongo),
|
||||
compose: many(compose),
|
||||
libsql: many(libsql),
|
||||
mariadb: many(mariadb),
|
||||
mongo: many(mongo),
|
||||
mysql: many(mysql),
|
||||
postgres: many(postgres),
|
||||
redis: many(redis),
|
||||
}),
|
||||
);
|
||||
|
||||
const createSchema = createInsertSchema(environments, {
|
||||
export const apiCreateEnvironment = z.object({
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneEnvironment = z.object({
|
||||
environmentId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiRemoveEnvironment = z.object({
|
||||
environmentId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateEnvironment = z.object({
|
||||
environmentId: z.string().min(1),
|
||||
name: z.string().min(1).optional(),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiDuplicateEnvironment = z.object({
|
||||
environmentId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateEnvironment = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
projectId: true,
|
||||
});
|
||||
|
||||
export const apiFindOneEnvironment = createSchema
|
||||
.pick({
|
||||
environmentId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRemoveEnvironment = createSchema
|
||||
.pick({
|
||||
environmentId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateEnvironment = createSchema.partial().extend({
|
||||
environmentId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiDuplicateEnvironment = createSchema
|
||||
.pick({
|
||||
environmentId: true,
|
||||
name: true,
|
||||
description: true,
|
||||
})
|
||||
.required({
|
||||
environmentId: true,
|
||||
name: true,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
@@ -8,7 +7,7 @@ import { bitbucket } from "./bitbucket";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { users_temp } from "./user";
|
||||
import { user } from "./user";
|
||||
|
||||
export const gitProviderType = pgEnum("gitProviderType", [
|
||||
"github",
|
||||
@@ -32,7 +31,7 @@ export const gitProvider = pgTable("git_provider", {
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
@@ -56,16 +55,12 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
fields: [gitProvider.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
user: one(user, {
|
||||
fields: [gitProvider.userId],
|
||||
references: [users_temp.id],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(gitProvider);
|
||||
|
||||
export const apiRemoveGitProvider = createSchema
|
||||
.extend({
|
||||
gitProviderId: z.string().min(1),
|
||||
})
|
||||
.pick({ gitProviderId: true });
|
||||
export const apiRemoveGitProvider = z.object({
|
||||
gitProviderId: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const gitea = pgTable("gitea", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(),
|
||||
giteaInternalUrl: text("giteaInternalUrl"),
|
||||
redirectUri: text("redirect_uri"),
|
||||
clientId: text("client_id"),
|
||||
clientSecret: text("client_secret"),
|
||||
@@ -40,6 +41,7 @@ export const apiCreateGitea = createSchema.extend({
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaInternalUrl: z.string().optional().nullable(),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
@@ -76,6 +78,7 @@ export const apiUpdateGitea = createSchema.extend({
|
||||
name: z.string().min(1),
|
||||
giteaId: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaInternalUrl: z.string().optional().nullable(),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { gitProvider } from "./git-provider";
|
||||
@@ -29,8 +28,7 @@ export const githubProviderRelations = relations(github, ({ one }) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(github);
|
||||
export const apiCreateGithub = createSchema.extend({
|
||||
export const apiCreateGithub = z.object({
|
||||
githubAppName: z.string().optional(),
|
||||
githubAppId: z.number().optional(),
|
||||
githubClientId: z.string().optional(),
|
||||
@@ -48,13 +46,11 @@ export const apiFindGithubBranches = z.object({
|
||||
githubId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneGithub = createSchema
|
||||
.extend({
|
||||
githubId: z.string().min(1),
|
||||
})
|
||||
.pick({ githubId: true });
|
||||
export const apiFindOneGithub = z.object({
|
||||
githubId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateGithub = createSchema.extend({
|
||||
export const apiUpdateGithub = z.object({
|
||||
githubId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
gitProviderId: z.string().min(1),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { gitProvider } from "./git-provider";
|
||||
@@ -11,6 +10,7 @@ export const gitlab = pgTable("gitlab", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
gitlabUrl: text("gitlabUrl").default("https://gitlab.com").notNull(),
|
||||
gitlabInternalUrl: text("gitlabInternalUrl"),
|
||||
applicationId: text("application_id"),
|
||||
redirectUri: text("redirect_uri"),
|
||||
secret: text("secret"),
|
||||
@@ -30,9 +30,7 @@ export const gitlabProviderRelations = relations(gitlab, ({ one }) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(gitlab);
|
||||
|
||||
export const apiCreateGitlab = createSchema.extend({
|
||||
export const apiCreateGitlab = z.object({
|
||||
applicationId: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
groupName: z.string().optional(),
|
||||
@@ -41,19 +39,17 @@ export const apiCreateGitlab = createSchema.extend({
|
||||
authId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
gitlabUrl: z.string().min(1),
|
||||
gitlabInternalUrl: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export const apiFindOneGitlab = createSchema
|
||||
.extend({
|
||||
gitlabId: z.string().min(1),
|
||||
})
|
||||
.pick({ gitlabId: true });
|
||||
export const apiFindOneGitlab = z.object({
|
||||
gitlabId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiGitlabTestConnection = createSchema
|
||||
.extend({
|
||||
groupName: z.string().optional(),
|
||||
})
|
||||
.pick({ gitlabId: true, groupName: true });
|
||||
export const apiGitlabTestConnection = z.object({
|
||||
gitlabId: z.string().min(1),
|
||||
groupName: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiFindGitlabBranches = z.object({
|
||||
id: z.number().optional(),
|
||||
@@ -62,7 +58,7 @@ export const apiFindGitlabBranches = z.object({
|
||||
gitlabId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateGitlab = createSchema.extend({
|
||||
export const apiUpdateGitlab = z.object({
|
||||
applicationId: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
groupName: z.string().optional(),
|
||||
@@ -70,4 +66,6 @@ export const apiUpdateGitlab = createSchema.extend({
|
||||
name: z.string().min(1),
|
||||
gitlabId: z.string().min(1),
|
||||
gitlabUrl: z.string().min(1),
|
||||
gitProviderId: z.string().min(1),
|
||||
gitlabInternalUrl: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./account";
|
||||
export * from "./ai";
|
||||
export * from "./audit-log";
|
||||
export * from "./application";
|
||||
export * from "./backups";
|
||||
export * from "./bitbucket";
|
||||
@@ -13,11 +14,13 @@ export * from "./git-provider";
|
||||
export * from "./gitea";
|
||||
export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./libsql";
|
||||
export * from "./mariadb";
|
||||
export * from "./mongo";
|
||||
export * from "./mount";
|
||||
export * from "./mysql";
|
||||
export * from "./notification";
|
||||
export * from "./patch";
|
||||
export * from "./port";
|
||||
export * from "./postgres";
|
||||
export * from "./preview-deployments";
|
||||
@@ -32,6 +35,9 @@ export * from "./server";
|
||||
export * from "./session";
|
||||
export * from "./shared";
|
||||
export * from "./ssh-key";
|
||||
export * from "./sso";
|
||||
export * from "./tag";
|
||||
export * from "./user";
|
||||
export * from "./utils";
|
||||
export * from "./volume-backups";
|
||||
export * from "./web-server-settings";
|
||||
|
||||
249
packages/server/src/db/schema/libsql.ts
Normal file
249
packages/server/src/db/schema/libsql.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
integer,
|
||||
json,
|
||||
pgTable,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { backups } from "./backups";
|
||||
import { environments } from "./environment";
|
||||
import { mounts } from "./mount";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
type EndpointSpecSwarm,
|
||||
EndpointSpecSwarmSchema,
|
||||
type HealthCheckSwarm,
|
||||
HealthCheckSwarmSchema,
|
||||
type LabelsSwarm,
|
||||
LabelsSwarmSchema,
|
||||
type NetworkSwarm,
|
||||
NetworkSwarmSchema,
|
||||
type PlacementSwarm,
|
||||
PlacementSwarmSchema,
|
||||
type RestartPolicySwarm,
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
sqldNode,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const libsql = pgTable("libsql", {
|
||||
libsqlId: text("libsqlId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("libsql"))
|
||||
.unique(),
|
||||
description: text("description"),
|
||||
databaseUser: text("databaseUser").notNull(),
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
sqldNode: sqldNode("sqldNode").notNull().default("primary"),
|
||||
sqldPrimaryUrl: text("sqldPrimaryUrl"),
|
||||
enableNamespaces: boolean("enableNamespaces").notNull().default(false),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
// RESOURCES
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
//
|
||||
externalPort: integer("externalPort"),
|
||||
externalGRPCPort: integer("externalGRPCPort"),
|
||||
externalAdminPort: integer("externalAdminPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
|
||||
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
|
||||
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
|
||||
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
environmentId: text("environmentId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const libsqlRelations = relations(libsql, ({ one, many }) => ({
|
||||
environment: one(environments, {
|
||||
fields: [libsql.environmentId],
|
||||
references: [environments.environmentId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
server: one(server, {
|
||||
fields: [libsql.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(libsql, {
|
||||
libsqlId: z.string(),
|
||||
name: z.string().min(1),
|
||||
appName: z.string().min(1),
|
||||
createdAt: z.string(),
|
||||
databaseUser: z.string().min(1),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
sqldNode: z.enum(sqldNode.enumValues),
|
||||
sqldPrimaryUrl: z.string().nullable(),
|
||||
enableNamespaces: z.boolean().default(false),
|
||||
dockerImage: z
|
||||
.string()
|
||||
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
externalGRPCPort: z.number(),
|
||||
externalAdminPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
placementSwarm: PlacementSwarmSchema.nullable(),
|
||||
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
|
||||
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateLibsql = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
sqldNode: true,
|
||||
sqldPrimaryUrl: true,
|
||||
enableNamespaces: true,
|
||||
serverId: true,
|
||||
})
|
||||
.required()
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.sqldNode === "replica" && !data.sqldPrimaryUrl) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["sqldPrimaryUrl"],
|
||||
message: "sqldPrimaryUrl is required when sqldNode is 'replica'.",
|
||||
});
|
||||
}
|
||||
if (data.sqldNode !== "replica" && data.sqldPrimaryUrl) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["sqldPrimaryUrl"],
|
||||
message:
|
||||
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const apiFindOneLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiChangeLibsqlStatus = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
applicationStatus: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveEnvironmentVariablesLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
env: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveExternalPortsLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
externalPort: true,
|
||||
externalGRPCPort: true,
|
||||
externalAdminPort: true,
|
||||
})
|
||||
.required({ libsqlId: true })
|
||||
.superRefine((data, ctx) => {
|
||||
if (
|
||||
data.externalPort === null &&
|
||||
data.externalGRPCPort === null &&
|
||||
data.externalAdminPort === null
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
"Either externalPort, externalGRPCPort or externalAdminPort must be provided.",
|
||||
path: ["externalPort", "externalGRPCPort", "externalAdminPort"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const apiDeployLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiResetLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateLibsql = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
libsqlId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
})
|
||||
.required();
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -9,6 +9,8 @@ import { mounts } from "./mount";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
type EndpointSpecSwarm,
|
||||
EndpointSpecSwarmSchema,
|
||||
type HealthCheckSwarm,
|
||||
HealthCheckSwarmSchema,
|
||||
type LabelsSwarm,
|
||||
@@ -21,10 +23,12 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { generateAppName } from "./utils";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
|
||||
export const mariadb = pgTable("mariadb", {
|
||||
mariadbId: text("mariadbId")
|
||||
@@ -43,6 +47,7 @@ export const mariadb = pgTable("mariadb", {
|
||||
databaseRootPassword: text("rootPassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
// RESOURCES
|
||||
memoryReservation: text("memoryReservation"),
|
||||
@@ -62,6 +67,9 @@ export const mariadb = pgTable("mariadb", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -91,7 +99,12 @@ export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
|
||||
const createSchema = createInsertSchema(mariadb, {
|
||||
mariadbId: z.string(),
|
||||
name: z.string().min(1),
|
||||
appName: z.string().min(1),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(63)
|
||||
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
|
||||
.optional(),
|
||||
createdAt: z.string(),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
@@ -110,6 +123,7 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
.optional(),
|
||||
dockerImage: z.string().default("mariadb:6"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
@@ -128,28 +142,27 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateMariaDB = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
databaseRootPassword: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
serverId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiCreateMariaDB = createSchema.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
databaseRootPassword: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
serverId: true,
|
||||
});
|
||||
|
||||
export const apiFindOneMariaDB = createSchema
|
||||
.pick({
|
||||
mariadbId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneMariaDB = z.object({
|
||||
mariadbId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiChangeMariaDBStatus = createSchema
|
||||
.pick({
|
||||
@@ -189,6 +202,7 @@ export const apiUpdateMariaDB = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
mariadbId: z.string().min(1),
|
||||
dockerImage: z.string().optional(),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
integer,
|
||||
json,
|
||||
pgTable,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -9,6 +16,8 @@ import { mounts } from "./mount";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
type EndpointSpecSwarm,
|
||||
EndpointSpecSwarmSchema,
|
||||
type HealthCheckSwarm,
|
||||
HealthCheckSwarmSchema,
|
||||
type LabelsSwarm,
|
||||
@@ -21,10 +30,12 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { generateAppName } from "./utils";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
|
||||
export const mongo = pgTable("mongo", {
|
||||
mongoId: text("mongoId")
|
||||
@@ -41,6 +52,7 @@ export const mongo = pgTable("mongo", {
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
@@ -58,6 +70,9 @@ export const mongo = pgTable("mongo", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -86,7 +101,12 @@ export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(mongo, {
|
||||
appName: z.string().min(1),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(63)
|
||||
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
|
||||
.optional(),
|
||||
createdAt: z.string(),
|
||||
mongoId: z.string(),
|
||||
name: z.string().min(1),
|
||||
@@ -99,6 +119,7 @@ const createSchema = createInsertSchema(mongo, {
|
||||
databaseUser: z.string().min(1),
|
||||
dockerImage: z.string().default("mongo:15"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
@@ -118,27 +139,26 @@ const createSchema = createInsertSchema(mongo, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateMongo = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
serverId: true,
|
||||
replicaSets: true,
|
||||
})
|
||||
.required();
|
||||
export const apiCreateMongo = createSchema.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
serverId: true,
|
||||
replicaSets: true,
|
||||
});
|
||||
|
||||
export const apiFindOneMongo = createSchema
|
||||
.pick({
|
||||
mongoId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneMongo = z.object({
|
||||
mongoId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiChangeMongoStatus = createSchema
|
||||
.pick({
|
||||
@@ -171,6 +191,7 @@ export const apiUpdateMongo = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
mongoId: z.string().min(1),
|
||||
dockerImage: z.string().optional(),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -19,8 +20,11 @@ export const serviceType = pgEnum("serviceType", [
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
"libsql",
|
||||
]);
|
||||
|
||||
export type ServiceType = (typeof serviceType.enumValues)[number];
|
||||
|
||||
export const mountType = pgEnum("mountType", ["bind", "volume", "file"]);
|
||||
|
||||
export const mounts = pgTable("mount", {
|
||||
@@ -39,7 +43,10 @@ export const mounts = pgTable("mount", {
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
postgresId: text("postgresId").references(() => postgres.postgresId, {
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mariadbId: text("mariadbId").references(() => mariadb.mariadbId, {
|
||||
@@ -51,10 +58,10 @@ export const mounts = pgTable("mount", {
|
||||
mysqlId: text("mysqlId").references(() => mysql.mysqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
postgresId: text("postgresId").references(() => postgres.postgresId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
@@ -64,9 +71,13 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
|
||||
fields: [mounts.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [mounts.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
compose: one(compose, {
|
||||
fields: [mounts.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
libsql: one(libsql, {
|
||||
fields: [mounts.libsqlId],
|
||||
references: [libsql.libsqlId],
|
||||
}),
|
||||
mariadb: one(mariadb, {
|
||||
fields: [mounts.mariadbId],
|
||||
@@ -80,14 +91,14 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
|
||||
fields: [mounts.mysqlId],
|
||||
references: [mysql.mysqlId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [mounts.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
}),
|
||||
redis: one(redis, {
|
||||
fields: [mounts.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [mounts.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(mounts, {
|
||||
@@ -99,23 +110,18 @@ const createSchema = createInsertSchema(mounts, {
|
||||
mountPath: z.string().min(1),
|
||||
mountId: z.string().optional(),
|
||||
filePath: z.string().optional(),
|
||||
serviceType: z
|
||||
.enum([
|
||||
"application",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
])
|
||||
.default("application"),
|
||||
serviceType: z.enum([
|
||||
"application",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
"libsql",
|
||||
]),
|
||||
});
|
||||
|
||||
export type ServiceType = NonNullable<
|
||||
z.infer<typeof createSchema>["serviceType"]
|
||||
>;
|
||||
|
||||
export const apiCreateMount = createSchema
|
||||
.pick({
|
||||
type: true,
|
||||
@@ -123,18 +129,16 @@ export const apiCreateMount = createSchema
|
||||
volumeName: true,
|
||||
content: true,
|
||||
mountPath: true,
|
||||
serviceType: true,
|
||||
filePath: true,
|
||||
serviceType: true,
|
||||
})
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneMount = createSchema
|
||||
.pick({
|
||||
mountId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneMount = z.object({
|
||||
mountId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiRemoveMount = createSchema
|
||||
.pick({
|
||||
@@ -146,14 +150,13 @@ export const apiRemoveMount = createSchema
|
||||
.required();
|
||||
|
||||
export const apiFindMountByApplicationId = createSchema
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
})
|
||||
.pick({
|
||||
serviceId: true,
|
||||
serviceType: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateMount = createSchema.partial().extend({
|
||||
mountId: z.string().min(1),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -9,6 +9,8 @@ import { mounts } from "./mount";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
type EndpointSpecSwarm,
|
||||
EndpointSpecSwarmSchema,
|
||||
type HealthCheckSwarm,
|
||||
HealthCheckSwarmSchema,
|
||||
type LabelsSwarm,
|
||||
@@ -21,10 +23,12 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { generateAppName } from "./utils";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
|
||||
export const mysql = pgTable("mysql", {
|
||||
mysqlId: text("mysqlId")
|
||||
@@ -43,6 +47,7 @@ export const mysql = pgTable("mysql", {
|
||||
databaseRootPassword: text("rootPassword").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
@@ -60,6 +65,9 @@ export const mysql = pgTable("mysql", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -88,7 +96,12 @@ export const mysqlRelations = relations(mysql, ({ one, many }) => ({
|
||||
|
||||
const createSchema = createInsertSchema(mysql, {
|
||||
mysqlId: z.string(),
|
||||
appName: z.string().min(1),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(63)
|
||||
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
|
||||
.optional(),
|
||||
createdAt: z.string(),
|
||||
name: z.string().min(1),
|
||||
databaseName: z.string().min(1),
|
||||
@@ -108,6 +121,7 @@ const createSchema = createInsertSchema(mysql, {
|
||||
.optional(),
|
||||
dockerImage: z.string().default("mysql:8"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
@@ -125,28 +139,27 @@ const createSchema = createInsertSchema(mysql, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateMySql = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
databaseRootPassword: true,
|
||||
serverId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiCreateMySql = createSchema.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
databaseRootPassword: true,
|
||||
serverId: true,
|
||||
});
|
||||
|
||||
export const apiFindOneMySql = createSchema
|
||||
.pick({
|
||||
mysqlId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneMySql = z.object({
|
||||
mysqlId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiChangeMySqlStatus = createSchema
|
||||
.pick({
|
||||
@@ -186,6 +199,7 @@ export const apiUpdateMySql = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
mysqlId: z.string().min(1),
|
||||
dockerImage: z.string().optional(),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -10,8 +17,13 @@ export const notificationType = pgEnum("notificationType", [
|
||||
"telegram",
|
||||
"discord",
|
||||
"email",
|
||||
"resend",
|
||||
"gotify",
|
||||
"ntfy",
|
||||
"pushover",
|
||||
"custom",
|
||||
"lark",
|
||||
"teams",
|
||||
]);
|
||||
|
||||
export const notifications = pgTable("notification", {
|
||||
@@ -23,6 +35,7 @@ export const notifications = pgTable("notification", {
|
||||
appDeploy: boolean("appDeploy").notNull().default(false),
|
||||
appBuildError: boolean("appBuildError").notNull().default(false),
|
||||
databaseBackup: boolean("databaseBackup").notNull().default(false),
|
||||
volumeBackup: boolean("volumeBackup").notNull().default(false),
|
||||
dokployRestart: boolean("dokployRestart").notNull().default(false),
|
||||
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
|
||||
serverThreshold: boolean("serverThreshold").notNull().default(false),
|
||||
@@ -42,12 +55,27 @@ export const notifications = pgTable("notification", {
|
||||
emailId: text("emailId").references(() => email.emailId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
resendId: text("resendId").references(() => resend.resendId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
customId: text("customId").references(() => custom.customId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
larkId: text("larkId").references(() => lark.larkId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
pushoverId: text("pushoverId").references(() => pushover.pushoverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
teamsId: text("teamsId").references(() => teams.teamsId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
@@ -94,6 +122,16 @@ export const email = pgTable("email", {
|
||||
toAddresses: text("toAddress").array().notNull(),
|
||||
});
|
||||
|
||||
export const resend = pgTable("resend", {
|
||||
resendId: text("resendId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
apiKey: text("apiKey").notNull(),
|
||||
fromAddress: text("fromAddress").notNull(),
|
||||
toAddresses: text("toAddress").array().notNull(),
|
||||
});
|
||||
|
||||
export const gotify = pgTable("gotify", {
|
||||
gotifyId: text("gotifyId")
|
||||
.notNull()
|
||||
@@ -112,10 +150,47 @@ export const ntfy = pgTable("ntfy", {
|
||||
.$defaultFn(() => nanoid()),
|
||||
serverUrl: text("serverUrl").notNull(),
|
||||
topic: text("topic").notNull(),
|
||||
accessToken: text("accessToken").notNull(),
|
||||
accessToken: text("accessToken"),
|
||||
priority: integer("priority").notNull().default(3),
|
||||
});
|
||||
|
||||
export const custom = pgTable("custom", {
|
||||
customId: text("customId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
endpoint: text("endpoint").notNull(),
|
||||
headers: jsonb("headers").$type<Record<string, string>>(),
|
||||
});
|
||||
|
||||
export const lark = pgTable("lark", {
|
||||
larkId: text("larkId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
});
|
||||
|
||||
export const pushover = pgTable("pushover", {
|
||||
pushoverId: text("pushoverId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
userKey: text("userKey").notNull(),
|
||||
apiToken: text("apiToken").notNull(),
|
||||
priority: integer("priority").notNull().default(0),
|
||||
retry: integer("retry"),
|
||||
expire: integer("expire"),
|
||||
});
|
||||
|
||||
export const teams = pgTable("teams", {
|
||||
teamsId: text("teamsId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
});
|
||||
|
||||
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
slack: one(slack, {
|
||||
fields: [notifications.slackId],
|
||||
@@ -133,6 +208,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.emailId],
|
||||
references: [email.emailId],
|
||||
}),
|
||||
resend: one(resend, {
|
||||
fields: [notifications.resendId],
|
||||
references: [resend.resendId],
|
||||
}),
|
||||
gotify: one(gotify, {
|
||||
fields: [notifications.gotifyId],
|
||||
references: [gotify.gotifyId],
|
||||
@@ -141,6 +220,22 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.ntfyId],
|
||||
references: [ntfy.ntfyId],
|
||||
}),
|
||||
custom: one(custom, {
|
||||
fields: [notifications.customId],
|
||||
references: [custom.customId],
|
||||
}),
|
||||
lark: one(lark, {
|
||||
fields: [notifications.larkId],
|
||||
references: [lark.larkId],
|
||||
}),
|
||||
pushover: one(pushover, {
|
||||
fields: [notifications.pushoverId],
|
||||
references: [pushover.pushoverId],
|
||||
}),
|
||||
teams: one(teams, {
|
||||
fields: [notifications.teamsId],
|
||||
references: [teams.teamsId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [notifications.organizationId],
|
||||
references: [organization.id],
|
||||
@@ -153,6 +248,7 @@ export const apiCreateSlack = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -180,6 +276,7 @@ export const apiCreateTelegram = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -209,6 +306,7 @@ export const apiCreateDiscord = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -239,6 +337,7 @@ export const apiCreateEmail = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -270,10 +369,41 @@ export const apiTestEmailConnection = apiCreateEmail.pick({
|
||||
fromAddress: true,
|
||||
});
|
||||
|
||||
export const apiCreateResend = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
apiKey: z.string().min(1),
|
||||
fromAddress: z.string().min(1),
|
||||
toAddresses: z.array(z.string()).min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateResend = apiCreateResend.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
resendId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestResendConnection = apiCreateResend.pick({
|
||||
apiKey: true,
|
||||
fromAddress: true,
|
||||
toAddresses: true,
|
||||
});
|
||||
|
||||
export const apiCreateGotify = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -307,6 +437,7 @@ export const apiCreateNtfy = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -315,7 +446,7 @@ export const apiCreateNtfy = notificationsSchema
|
||||
.extend({
|
||||
serverUrl: z.string().min(1),
|
||||
topic: z.string().min(1),
|
||||
accessToken: z.string().min(1),
|
||||
accessToken: z.string().optional(),
|
||||
priority: z.number().min(1),
|
||||
})
|
||||
.required();
|
||||
@@ -333,12 +464,152 @@ export const apiTestNtfyConnection = apiCreateNtfy.pick({
|
||||
priority: true,
|
||||
});
|
||||
|
||||
export const apiFindOneNotification = notificationsSchema
|
||||
export const apiFindOneNotification = z.object({
|
||||
notificationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateCustom = notificationsSchema
|
||||
.pick({
|
||||
notificationId: true,
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
endpoint: z.string().min(1),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateCustom = apiCreateCustom.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
customId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestCustomConnection = z.object({
|
||||
endpoint: z.string().min(1),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
|
||||
export const apiCreateLark = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateLark = apiCreateLark.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
larkId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestLarkConnection = apiCreateLark.pick({
|
||||
webhookUrl: true,
|
||||
});
|
||||
|
||||
export const apiCreateTeams = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateTeams = apiCreateTeams.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
teamsId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestTeamsConnection = apiCreateTeams.pick({
|
||||
webhookUrl: true,
|
||||
});
|
||||
|
||||
export const apiCreatePushover = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
userKey: z.string().min(1),
|
||||
apiToken: z.string().min(1),
|
||||
priority: z.number().min(-2).max(2).default(0),
|
||||
retry: z.number().min(30).nullish(),
|
||||
expire: z.number().min(1).max(10800).nullish(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.priority !== 2 || (data.retry != null && data.expire != null),
|
||||
{
|
||||
message: "Retry and expire are required for emergency priority (2)",
|
||||
path: ["retry"],
|
||||
},
|
||||
);
|
||||
|
||||
export const apiUpdatePushover = z.object({
|
||||
notificationId: z.string().min(1),
|
||||
pushoverId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
userKey: z.string().min(1).optional(),
|
||||
apiToken: z.string().min(1).optional(),
|
||||
priority: z.number().min(-2).max(2).optional(),
|
||||
retry: z.number().min(30).nullish(),
|
||||
expire: z.number().min(1).max(10800).nullish(),
|
||||
appBuildError: z.boolean().optional(),
|
||||
databaseBackup: z.boolean().optional(),
|
||||
volumeBackup: z.boolean().optional(),
|
||||
dokployRestart: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
appDeploy: z.boolean().optional(),
|
||||
dockerCleanup: z.boolean().optional(),
|
||||
serverThreshold: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiTestPushoverConnection = z
|
||||
.object({
|
||||
userKey: z.string().min(1),
|
||||
apiToken: z.string().min(1),
|
||||
priority: z.number().min(-2).max(2),
|
||||
retry: z.number().min(30).nullish(),
|
||||
expire: z.number().min(1).max(10800).nullish(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.priority !== 2 || (data.retry != null && data.expire != null),
|
||||
{
|
||||
message: "Retry and expire are required for emergency priority (2)",
|
||||
path: ["retry"],
|
||||
},
|
||||
);
|
||||
|
||||
export const apiSendTest = notificationsSchema
|
||||
.extend({
|
||||
botToken: z.string(),
|
||||
@@ -351,10 +622,13 @@ export const apiSendTest = notificationsSchema
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
toAddresses: z.array(z.string()),
|
||||
apiKey: z.string(),
|
||||
serverUrl: z.string(),
|
||||
topic: z.string(),
|
||||
appToken: z.string(),
|
||||
accessToken: z.string(),
|
||||
accessToken: z.string().optional(),
|
||||
priority: z.number(),
|
||||
endpoint: z.string(),
|
||||
headers: z.string(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
100
packages/server/src/db/schema/patch.ts
Normal file
100
packages/server/src/db/schema/patch.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, pgEnum, pgTable, text, unique } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
|
||||
export const patchType = pgEnum("patchType", ["create", "update", "delete"]);
|
||||
|
||||
export const patch = pgTable(
|
||||
"patch",
|
||||
{
|
||||
patchId: text("patchId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
type: patchType("type").notNull().default("update"),
|
||||
filePath: text("filePath").notNull(),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
content: text("content").notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
updatedAt: text("updatedAt").$defaultFn(() => new Date().toISOString()),
|
||||
// Relations - one of these must be set
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
},
|
||||
(table) => [
|
||||
// Unique constraint: one patch per file per application/compose
|
||||
unique("patch_filepath_application_unique").on(
|
||||
table.filePath,
|
||||
table.applicationId,
|
||||
),
|
||||
unique("patch_filepath_compose_unique").on(table.filePath, table.composeId),
|
||||
],
|
||||
);
|
||||
|
||||
export const patchRelations = relations(patch, ({ one }) => ({
|
||||
application: one(applications, {
|
||||
fields: [patch.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [patch.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(patch, {
|
||||
filePath: z.string().min(1),
|
||||
content: z.string(),
|
||||
type: z.enum(["create", "update", "delete"]).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
applicationId: z.string().optional(),
|
||||
composeId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreatePatch = createSchema.pick({
|
||||
filePath: true,
|
||||
content: true,
|
||||
type: true,
|
||||
enabled: true,
|
||||
applicationId: true,
|
||||
composeId: true,
|
||||
});
|
||||
|
||||
export const apiFindPatch = z.object({
|
||||
patchId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindPatchesByApplicationId = z.object({
|
||||
applicationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindPatchesByComposeId = z.object({
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdatePatch = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
patchId: z.string().min(1),
|
||||
})
|
||||
.omit({ applicationId: true, composeId: true });
|
||||
|
||||
export const apiDeletePatch = z.object({
|
||||
patchId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiTogglePatchEnabled = z.object({
|
||||
patchId: z.string().min(1),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
@@ -49,11 +49,9 @@ export const apiCreatePort = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOnePort = createSchema
|
||||
.pick({
|
||||
portId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOnePort = z.object({
|
||||
portId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdatePort = createSchema
|
||||
.pick({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -9,6 +9,8 @@ import { mounts } from "./mount";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
type EndpointSpecSwarm,
|
||||
EndpointSpecSwarmSchema,
|
||||
type HealthCheckSwarm,
|
||||
HealthCheckSwarmSchema,
|
||||
type LabelsSwarm,
|
||||
@@ -21,10 +23,12 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { generateAppName } from "./utils";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
|
||||
export const postgres = pgTable("postgres", {
|
||||
postgresId: text("postgresId")
|
||||
@@ -42,6 +46,7 @@ export const postgres = pgTable("postgres", {
|
||||
description: text("description"),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
externalPort: integer("externalPort"),
|
||||
@@ -60,6 +65,9 @@ export const postgres = pgTable("postgres", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -89,6 +97,12 @@ export const postgresRelations = relations(postgres, ({ one, many }) => ({
|
||||
const createSchema = createInsertSchema(postgres, {
|
||||
postgresId: z.string(),
|
||||
name: z.string().min(1),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(63)
|
||||
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
|
||||
.optional(),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
@@ -97,8 +111,9 @@ const createSchema = createInsertSchema(postgres, {
|
||||
}),
|
||||
databaseName: z.string().min(1),
|
||||
databaseUser: z.string().min(1),
|
||||
dockerImage: z.string().default("postgres:15"),
|
||||
dockerImage: z.string().default("postgres:18"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
@@ -118,27 +133,26 @@ const createSchema = createInsertSchema(postgres, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreatePostgres = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiCreatePostgres = createSchema.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
databaseName: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
});
|
||||
|
||||
export const apiFindOnePostgres = createSchema
|
||||
.pick({
|
||||
postgresId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOnePostgres = z.object({
|
||||
postgresId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiChangePostgresStatus = createSchema
|
||||
.pick({
|
||||
@@ -178,6 +192,7 @@ export const apiUpdatePostgres = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
postgresId: z.string().min(1),
|
||||
dockerImage: z.string().optional(),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
|
||||
@@ -58,17 +58,12 @@ export const createSchema = createInsertSchema(previewDeployments, {
|
||||
applicationId: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreatePreviewDeployment = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
domainId: true,
|
||||
branch: true,
|
||||
pullRequestId: true,
|
||||
pullRequestNumber: true,
|
||||
pullRequestURL: true,
|
||||
pullRequestTitle: true,
|
||||
})
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
// deploymentId: z.string().min(1),
|
||||
});
|
||||
export const apiCreatePreviewDeployment = z.object({
|
||||
applicationId: z.string().min(1),
|
||||
domainId: z.string().optional(),
|
||||
branch: z.string().min(1),
|
||||
pullRequestId: z.string().min(1),
|
||||
pullRequestNumber: z.string().min(1),
|
||||
pullRequestURL: z.string().min(1),
|
||||
pullRequestTitle: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { environments } from "./environment";
|
||||
import { projectTags } from "./tag";
|
||||
|
||||
export const projects = pgTable("project", {
|
||||
projectId: text("projectId")
|
||||
@@ -25,6 +26,7 @@ export const projects = pgTable("project", {
|
||||
|
||||
export const projectRelations = relations(projects, ({ many, one }) => ({
|
||||
environments: many(environments),
|
||||
projectTags: many(projectTags),
|
||||
organization: one(organization, {
|
||||
fields: [projects.organizationId],
|
||||
references: [organization.id],
|
||||
@@ -43,12 +45,9 @@ export const apiCreateProject = createSchema.pick({
|
||||
env: true,
|
||||
});
|
||||
|
||||
export const apiFindOneProject = createSchema
|
||||
.pick({
|
||||
projectId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneProject = z.object({
|
||||
projectId: z.string().min(1),
|
||||
});
|
||||
export const apiRemoveProject = createSchema
|
||||
.pick({
|
||||
projectId: true,
|
||||
|
||||
@@ -35,11 +35,9 @@ const createSchema = createInsertSchema(redirects, {
|
||||
permanent: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneRedirect = createSchema
|
||||
.pick({
|
||||
redirectId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneRedirect = z.object({
|
||||
redirectId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateRedirect = createSchema
|
||||
.pick({
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { environments } from "./environment";
|
||||
import { mounts } from "./mount";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
type EndpointSpecSwarm,
|
||||
EndpointSpecSwarmSchema,
|
||||
type HealthCheckSwarm,
|
||||
HealthCheckSwarmSchema,
|
||||
type LabelsSwarm,
|
||||
@@ -21,10 +22,12 @@ import {
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
type UlimitsSwarm,
|
||||
UlimitsSwarmSchema,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { generateAppName } from "./utils";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
|
||||
|
||||
export const redis = pgTable("redis", {
|
||||
redisId: text("redisId")
|
||||
@@ -40,6 +43,7 @@ export const redis = pgTable("redis", {
|
||||
databasePassword: text("password").notNull(),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
args: text("args").array(),
|
||||
env: text("env"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
@@ -60,6 +64,9 @@ export const redis = pgTable("redis", {
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
ulimitsSwarm: json("ulimitsSwarm").$type<UlimitsSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
|
||||
environmentId: text("environmentId")
|
||||
@@ -84,12 +91,18 @@ export const redisRelations = relations(redis, ({ one, many }) => ({
|
||||
|
||||
const createSchema = createInsertSchema(redis, {
|
||||
redisId: z.string(),
|
||||
appName: z.string().min(1),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(63)
|
||||
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
|
||||
.optional(),
|
||||
createdAt: z.string(),
|
||||
name: z.string().min(1),
|
||||
databasePassword: z.string(),
|
||||
dockerImage: z.string().default("redis:8"),
|
||||
command: z.string().optional(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
@@ -108,25 +121,24 @@ const createSchema = createInsertSchema(redis, {
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateRedis = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
databasePassword: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiCreateRedis = createSchema.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
databasePassword: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
});
|
||||
|
||||
export const apiFindOneRedis = createSchema
|
||||
.pick({
|
||||
redisId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneRedis = z.object({
|
||||
redisId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiChangeRedisStatus = createSchema
|
||||
.pick({
|
||||
@@ -166,6 +178,7 @@ export const apiUpdateRedis = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
redisId: z.string().min(1),
|
||||
dockerImage: z.string().optional(),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
|
||||
@@ -33,7 +33,15 @@ export const registry = pgTable("registry", {
|
||||
});
|
||||
|
||||
export const registryRelations = relations(registry, ({ many }) => ({
|
||||
applications: many(applications),
|
||||
applications: many(applications, {
|
||||
relationName: "applicationRegistry",
|
||||
}),
|
||||
buildApplications: many(applications, {
|
||||
relationName: "applicationBuildRegistry",
|
||||
}),
|
||||
rollbackApplications: many(applications, {
|
||||
relationName: "applicationRollbackRegistry",
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(registry, {
|
||||
@@ -72,17 +80,23 @@ export const apiTestRegistry = createSchema.pick({}).extend({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestRegistryById = createSchema
|
||||
.pick({
|
||||
registryId: true,
|
||||
})
|
||||
.extend({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiRemoveRegistry = createSchema
|
||||
.pick({
|
||||
registryId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneRegistry = createSchema
|
||||
.pick({
|
||||
registryId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneRegistry = z.object({
|
||||
registryId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateRegistry = createSchema.partial().extend({
|
||||
registryId: z.string().min(1),
|
||||
|
||||
@@ -7,7 +7,7 @@ import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { server } from "./server";
|
||||
import { users_temp } from "./user";
|
||||
import { user } from "./user";
|
||||
import { generateAppName } from "./utils";
|
||||
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
||||
|
||||
@@ -45,10 +45,11 @@ export const schedules = pgTable("schedule", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => users_temp.id, {
|
||||
userId: text("userId").references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
timezone: text("timezone"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -69,9 +70,9 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
|
||||
fields: [schedules.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
user: one(user, {
|
||||
fields: [schedules.userId],
|
||||
references: [users_temp.id],
|
||||
references: [user.id],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
}));
|
||||
|
||||
@@ -5,17 +5,24 @@ enum applicationStatus {
|
||||
error
|
||||
}
|
||||
|
||||
enum backupType {
|
||||
database
|
||||
compose
|
||||
}
|
||||
|
||||
enum buildType {
|
||||
dockerfile
|
||||
heroku_buildpacks
|
||||
paketo_buildpacks
|
||||
nixpacks
|
||||
static
|
||||
railpack
|
||||
}
|
||||
|
||||
enum certificateType {
|
||||
letsencrypt
|
||||
none
|
||||
custom
|
||||
}
|
||||
|
||||
enum composeType {
|
||||
@@ -28,6 +35,7 @@ enum databaseType {
|
||||
mariadb
|
||||
mysql
|
||||
mongo
|
||||
"web-server"
|
||||
}
|
||||
|
||||
enum deploymentStatus {
|
||||
@@ -61,6 +69,8 @@ enum notificationType {
|
||||
discord
|
||||
email
|
||||
gotify
|
||||
ntfy
|
||||
custom
|
||||
}
|
||||
|
||||
enum protocolType {
|
||||
@@ -68,14 +78,21 @@ enum protocolType {
|
||||
udp
|
||||
}
|
||||
|
||||
enum publishModeType {
|
||||
ingress
|
||||
host
|
||||
}
|
||||
|
||||
enum RegistryType {
|
||||
selfHosted
|
||||
cloud
|
||||
}
|
||||
|
||||
enum Roles {
|
||||
admin
|
||||
user
|
||||
enum scheduleType {
|
||||
application
|
||||
compose
|
||||
server
|
||||
"dokploy-server"
|
||||
}
|
||||
|
||||
enum serverStatus {
|
||||
@@ -93,6 +110,11 @@ enum serviceType {
|
||||
compose
|
||||
}
|
||||
|
||||
enum shellType {
|
||||
bash
|
||||
sh
|
||||
}
|
||||
|
||||
enum sourceType {
|
||||
docker
|
||||
git
|
||||
@@ -112,6 +134,11 @@ enum sourceTypeCompose {
|
||||
raw
|
||||
}
|
||||
|
||||
enum triggerType {
|
||||
push
|
||||
tag
|
||||
}
|
||||
|
||||
table account {
|
||||
id text [pk, not null]
|
||||
account_id text [not null]
|
||||
@@ -133,7 +160,39 @@ table account {
|
||||
confirmationExpiresAt text
|
||||
}
|
||||
|
||||
table admin {
|
||||
table ai {
|
||||
aiId text [pk, not null]
|
||||
name text [not null]
|
||||
apiUrl text [not null]
|
||||
apiKey text [not null]
|
||||
model text [not null]
|
||||
isEnabled boolean [not null, default: true]
|
||||
organizationId text [not null]
|
||||
createdAt text [not null]
|
||||
}
|
||||
|
||||
table apikey {
|
||||
id text [pk, not null]
|
||||
name text
|
||||
start text
|
||||
prefix text
|
||||
key text [not null]
|
||||
user_id text [not null]
|
||||
refill_interval integer
|
||||
refill_amount integer
|
||||
last_refill_at timestamp
|
||||
enabled boolean
|
||||
rate_limit_enabled boolean
|
||||
rate_limit_time_window integer
|
||||
rate_limit_max integer
|
||||
request_count integer
|
||||
remaining integer
|
||||
last_request timestamp
|
||||
expires_at timestamp
|
||||
created_at timestamp [not null]
|
||||
updated_at timestamp [not null]
|
||||
permissions text
|
||||
metadata text
|
||||
}
|
||||
|
||||
table application {
|
||||
@@ -143,14 +202,19 @@ table application {
|
||||
description text
|
||||
env text
|
||||
previewEnv text
|
||||
watchPaths text[]
|
||||
previewBuildArgs text
|
||||
previewLabels text[]
|
||||
previewWildcard text
|
||||
previewPort integer [default: 3000]
|
||||
previewHttps boolean [not null, default: false]
|
||||
previewPath text [default: '/']
|
||||
certificateType certificateType [not null, default: 'none']
|
||||
previewCustomCertResolver text
|
||||
previewLimit integer [default: 3]
|
||||
isPreviewDeploymentsActive boolean [default: false]
|
||||
previewRequireCollaboratorPermissions boolean [default: true]
|
||||
rollbackActive boolean [default: false]
|
||||
buildArgs text
|
||||
memoryReservation text
|
||||
memoryLimit text
|
||||
@@ -167,6 +231,7 @@ table application {
|
||||
owner text
|
||||
branch text
|
||||
buildPath text [default: '/']
|
||||
triggerType triggerType [default: 'push']
|
||||
autoDeploy boolean
|
||||
gitlabProjectId integer
|
||||
gitlabRepository text
|
||||
@@ -174,6 +239,10 @@ table application {
|
||||
gitlabBranch text
|
||||
gitlabBuildPath text [default: '/']
|
||||
gitlabPathNamespace text
|
||||
giteaRepository text
|
||||
giteaOwner text
|
||||
giteaBranch text
|
||||
giteaBuildPath text [default: '/']
|
||||
bitbucketRepository text
|
||||
bitbucketOwner text
|
||||
bitbucketBranch text
|
||||
@@ -186,6 +255,7 @@ table application {
|
||||
customGitBranch text
|
||||
customGitBuildPath text
|
||||
customGitSSHKeyId text
|
||||
enableSubmodules boolean [not null, default: false]
|
||||
dockerfile text
|
||||
dockerContextPath text
|
||||
dockerBuildStage text
|
||||
@@ -201,52 +271,47 @@ table application {
|
||||
replicas integer [not null, default: 1]
|
||||
applicationStatus applicationStatus [not null, default: 'idle']
|
||||
buildType buildType [not null, default: 'nixpacks']
|
||||
railpackVersion text [default: '0.2.2']
|
||||
herokuVersion text [default: '24']
|
||||
publishDirectory text
|
||||
isStaticSpa boolean
|
||||
createdAt text [not null]
|
||||
registryId text
|
||||
projectId text [not null]
|
||||
environmentId text [not null]
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
giteaId text
|
||||
bitbucketId text
|
||||
serverId text
|
||||
}
|
||||
|
||||
table auth {
|
||||
id text [pk, not null]
|
||||
email text [not null, unique]
|
||||
password text [not null]
|
||||
rol Roles [not null]
|
||||
image text
|
||||
secret text
|
||||
token text
|
||||
is2FAEnabled boolean [not null, default: false]
|
||||
createdAt text [not null]
|
||||
resetPasswordToken text
|
||||
resetPasswordExpiresAt text
|
||||
confirmationToken text
|
||||
confirmationExpiresAt text
|
||||
}
|
||||
|
||||
table backup {
|
||||
backupId text [pk, not null]
|
||||
appName text [not null, unique]
|
||||
schedule text [not null]
|
||||
enabled boolean
|
||||
database text [not null]
|
||||
prefix text [not null]
|
||||
serviceName text
|
||||
destinationId text [not null]
|
||||
keepLatestCount integer
|
||||
backupType backupType [not null, default: 'database']
|
||||
databaseType databaseType [not null]
|
||||
composeId text
|
||||
postgresId text
|
||||
mariadbId text
|
||||
mysqlId text
|
||||
mongoId text
|
||||
userId text
|
||||
metadata jsonb
|
||||
}
|
||||
|
||||
table bitbucket {
|
||||
bitbucketId text [pk, not null]
|
||||
bitbucketUsername text
|
||||
bitbucketEmail text
|
||||
appPassword text
|
||||
apiToken text
|
||||
bitbucketWorkspaceName text
|
||||
gitProviderId text [not null]
|
||||
}
|
||||
@@ -258,7 +323,7 @@ table certificate {
|
||||
privateKey text [not null]
|
||||
certificatePath text [not null, unique]
|
||||
autoRenew boolean
|
||||
userId text
|
||||
organizationId text [not null]
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -291,13 +356,17 @@ table compose {
|
||||
customGitBranch text
|
||||
customGitSSHKeyId text
|
||||
command text [not null, default: '']
|
||||
enableSubmodules boolean [not null, default: false]
|
||||
composePath text [not null, default: './docker-compose.yml']
|
||||
suffix text [not null, default: '']
|
||||
randomize boolean [not null, default: false]
|
||||
isolatedDeployment boolean [not null, default: false]
|
||||
isolatedDeploymentsVolume boolean [not null, default: false]
|
||||
triggerType triggerType [default: 'push']
|
||||
composeStatus applicationStatus [not null, default: 'idle']
|
||||
projectId text [not null]
|
||||
environmentId text [not null]
|
||||
createdAt text [not null]
|
||||
watchPaths text[]
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
@@ -305,19 +374,32 @@ table compose {
|
||||
serverId text
|
||||
}
|
||||
|
||||
table custom {
|
||||
customId text [pk, not null]
|
||||
endpoint text [not null]
|
||||
headers text
|
||||
}
|
||||
|
||||
table deployment {
|
||||
deploymentId text [pk, not null]
|
||||
title text [not null]
|
||||
description text
|
||||
status deploymentStatus [default: 'running']
|
||||
logPath text [not null]
|
||||
pid text
|
||||
applicationId text
|
||||
composeId text
|
||||
serverId text
|
||||
isPreviewDeployment boolean [default: false]
|
||||
previewDeploymentId text
|
||||
createdAt text [not null]
|
||||
startedAt text
|
||||
finishedAt text
|
||||
errorMessage text
|
||||
scheduleId text
|
||||
backupId text
|
||||
rollbackId text
|
||||
volumeBackupId text
|
||||
}
|
||||
|
||||
table destination {
|
||||
@@ -329,7 +411,8 @@ table destination {
|
||||
bucket text [not null]
|
||||
region text [not null]
|
||||
endpoint text [not null]
|
||||
userId text [not null]
|
||||
organizationId text [not null]
|
||||
createdAt timestamp [not null, default: `now()`]
|
||||
}
|
||||
|
||||
table discord {
|
||||
@@ -349,9 +432,12 @@ table domain {
|
||||
uniqueConfigKey serial [not null, increment]
|
||||
createdAt text [not null]
|
||||
composeId text
|
||||
customCertResolver text
|
||||
applicationId text
|
||||
previewDeploymentId text
|
||||
certificateType certificateType [not null, default: 'none']
|
||||
internalPath text [default: '/']
|
||||
stripPath boolean [not null, default: false]
|
||||
}
|
||||
|
||||
table email {
|
||||
@@ -364,12 +450,37 @@ table email {
|
||||
toAddress text[] [not null]
|
||||
}
|
||||
|
||||
table environment {
|
||||
environmentId text [pk, not null]
|
||||
name text [not null]
|
||||
description text
|
||||
createdAt text [not null]
|
||||
env text [not null, default: '']
|
||||
projectId text [not null]
|
||||
}
|
||||
|
||||
table git_provider {
|
||||
gitProviderId text [pk, not null]
|
||||
name text [not null]
|
||||
providerType gitProviderType [not null, default: 'github']
|
||||
createdAt text [not null]
|
||||
userId text
|
||||
organizationId text [not null]
|
||||
userId text [not null]
|
||||
}
|
||||
|
||||
table gitea {
|
||||
giteaId text [pk, not null]
|
||||
giteaUrl text [not null, default: 'https://gitea.com']
|
||||
giteaInternalUrl text
|
||||
redirect_uri text
|
||||
client_id text
|
||||
client_secret text
|
||||
gitProviderId text [not null]
|
||||
access_token text
|
||||
refresh_token text
|
||||
expires_at integer
|
||||
scopes text [default: 'repo,repo:status,read:user,read:org']
|
||||
last_authenticated_at integer
|
||||
}
|
||||
|
||||
table github {
|
||||
@@ -387,6 +498,7 @@ table github {
|
||||
table gitlab {
|
||||
gitlabId text [pk, not null]
|
||||
gitlabUrl text [not null, default: 'https://gitlab.com']
|
||||
gitlabInternalUrl text
|
||||
application_id text
|
||||
redirect_uri text
|
||||
secret text
|
||||
@@ -397,20 +509,6 @@ table gitlab {
|
||||
gitProviderId text [not null]
|
||||
}
|
||||
|
||||
table gitea {
|
||||
giteaId text [pk, not null]
|
||||
giteaUrl text [not null, default: 'https://gitea.com']
|
||||
redirect_uri text
|
||||
client_id text [not null]
|
||||
client_secret text [not null]
|
||||
access_token text
|
||||
refresh_token text
|
||||
expires_at integer
|
||||
gitProviderId text [not null]
|
||||
scopes text [default: 'repo,repo:status,read:user,read:org']
|
||||
last_authenticated_at integer
|
||||
}
|
||||
|
||||
table gotify {
|
||||
gotifyId text [pk, not null]
|
||||
serverUrl text [not null]
|
||||
@@ -427,6 +525,7 @@ table invitation {
|
||||
status text [not null]
|
||||
expires_at timestamp [not null]
|
||||
inviter_id text [not null]
|
||||
team_id text
|
||||
}
|
||||
|
||||
table mariadb {
|
||||
@@ -447,8 +546,17 @@ table mariadb {
|
||||
cpuLimit text
|
||||
externalPort integer
|
||||
applicationStatus applicationStatus [not null, default: 'idle']
|
||||
healthCheckSwarm json
|
||||
restartPolicySwarm json
|
||||
placementSwarm json
|
||||
updateConfigSwarm json
|
||||
rollbackConfigSwarm json
|
||||
modeSwarm json
|
||||
labelsSwarm json
|
||||
networkSwarm json
|
||||
replicas integer [not null, default: 1]
|
||||
createdAt text [not null]
|
||||
projectId text [not null]
|
||||
environmentId text [not null]
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -458,6 +566,19 @@ table member {
|
||||
user_id text [not null]
|
||||
role text [not null]
|
||||
created_at timestamp [not null]
|
||||
team_id text
|
||||
canCreateProjects boolean [not null, default: false]
|
||||
canAccessToSSHKeys boolean [not null, default: false]
|
||||
canCreateServices boolean [not null, default: false]
|
||||
canDeleteProjects boolean [not null, default: false]
|
||||
canDeleteServices boolean [not null, default: false]
|
||||
canAccessToDocker boolean [not null, default: false]
|
||||
canAccessToAPI boolean [not null, default: false]
|
||||
canAccessToGitProviders boolean [not null, default: false]
|
||||
canAccessToTraefikFiles boolean [not null, default: false]
|
||||
accesedProjects text[] [not null, default: `ARRAY[]::text[]`]
|
||||
accessedEnvironments text[] [not null, default: `ARRAY[]::text[]`]
|
||||
accesedServices text[] [not null, default: `ARRAY[]::text[]`]
|
||||
}
|
||||
|
||||
table mongo {
|
||||
@@ -476,8 +597,17 @@ table mongo {
|
||||
cpuLimit text
|
||||
externalPort integer
|
||||
applicationStatus applicationStatus [not null, default: 'idle']
|
||||
healthCheckSwarm json
|
||||
restartPolicySwarm json
|
||||
placementSwarm json
|
||||
updateConfigSwarm json
|
||||
rollbackConfigSwarm json
|
||||
modeSwarm json
|
||||
labelsSwarm json
|
||||
networkSwarm json
|
||||
replicas integer [not null, default: 1]
|
||||
createdAt text [not null]
|
||||
projectId text [not null]
|
||||
environmentId text [not null]
|
||||
serverId text
|
||||
replicaSets boolean [default: false]
|
||||
}
|
||||
@@ -518,8 +648,17 @@ table mysql {
|
||||
cpuLimit text
|
||||
externalPort integer
|
||||
applicationStatus applicationStatus [not null, default: 'idle']
|
||||
healthCheckSwarm json
|
||||
restartPolicySwarm json
|
||||
placementSwarm json
|
||||
updateConfigSwarm json
|
||||
rollbackConfigSwarm json
|
||||
modeSwarm json
|
||||
labelsSwarm json
|
||||
networkSwarm json
|
||||
replicas integer [not null, default: 1]
|
||||
createdAt text [not null]
|
||||
projectId text [not null]
|
||||
environmentId text [not null]
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -539,7 +678,17 @@ table notification {
|
||||
discordId text
|
||||
emailId text
|
||||
gotifyId text
|
||||
userId text
|
||||
ntfyId text
|
||||
customId text
|
||||
organizationId text [not null]
|
||||
}
|
||||
|
||||
table ntfy {
|
||||
ntfyId text [pk, not null]
|
||||
serverUrl text [not null]
|
||||
topic text [not null]
|
||||
accessToken text [not null]
|
||||
priority integer [not null, default: 3]
|
||||
}
|
||||
|
||||
table organization {
|
||||
@@ -555,6 +704,7 @@ table organization {
|
||||
table port {
|
||||
portId text [pk, not null]
|
||||
publishedPort integer [not null]
|
||||
publishMode publishModeType [not null, default: 'host']
|
||||
targetPort integer [not null]
|
||||
protocol protocolType [not null]
|
||||
applicationId text [not null]
|
||||
@@ -577,8 +727,17 @@ table postgres {
|
||||
cpuReservation text
|
||||
cpuLimit text
|
||||
applicationStatus applicationStatus [not null, default: 'idle']
|
||||
healthCheckSwarm json
|
||||
restartPolicySwarm json
|
||||
placementSwarm json
|
||||
updateConfigSwarm json
|
||||
rollbackConfigSwarm json
|
||||
modeSwarm json
|
||||
labelsSwarm json
|
||||
networkSwarm json
|
||||
replicas integer [not null, default: 1]
|
||||
createdAt text [not null]
|
||||
projectId text [not null]
|
||||
environmentId text [not null]
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -603,7 +762,7 @@ table project {
|
||||
name text [not null]
|
||||
description text
|
||||
createdAt text [not null]
|
||||
userId text [not null]
|
||||
organizationId text [not null]
|
||||
env text [not null, default: '']
|
||||
}
|
||||
|
||||
@@ -633,7 +792,16 @@ table redis {
|
||||
externalPort integer
|
||||
createdAt text [not null]
|
||||
applicationStatus applicationStatus [not null, default: 'idle']
|
||||
projectId text [not null]
|
||||
healthCheckSwarm json
|
||||
restartPolicySwarm json
|
||||
placementSwarm json
|
||||
updateConfigSwarm json
|
||||
rollbackConfigSwarm json
|
||||
modeSwarm json
|
||||
labelsSwarm json
|
||||
networkSwarm json
|
||||
replicas integer [not null, default: 1]
|
||||
environmentId text [not null]
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -646,7 +814,34 @@ table registry {
|
||||
registryUrl text [not null, default: '']
|
||||
createdAt text [not null]
|
||||
selfHosted RegistryType [not null, default: 'cloud']
|
||||
userId text [not null]
|
||||
organizationId text [not null]
|
||||
}
|
||||
|
||||
table rollback {
|
||||
rollbackId text [pk, not null]
|
||||
deploymentId text [not null]
|
||||
version serial [not null, increment]
|
||||
image text
|
||||
createdAt text [not null]
|
||||
fullContext jsonb
|
||||
}
|
||||
|
||||
table schedule {
|
||||
scheduleId text [pk, not null]
|
||||
name text [not null]
|
||||
cronExpression text [not null]
|
||||
appName text [not null]
|
||||
serviceName text
|
||||
shellType shellType [not null, default: 'bash']
|
||||
scheduleType scheduleType [not null, default: 'application']
|
||||
command text [not null]
|
||||
script text
|
||||
applicationId text
|
||||
composeId text
|
||||
serverId text
|
||||
userId text
|
||||
enabled boolean [not null, default: true]
|
||||
createdAt text [not null]
|
||||
}
|
||||
|
||||
table security {
|
||||
@@ -671,14 +866,14 @@ table server {
|
||||
appName text [not null]
|
||||
enableDockerCleanup boolean [not null, default: false]
|
||||
createdAt text [not null]
|
||||
userId text [not null]
|
||||
organizationId text [not null]
|
||||
serverStatus serverStatus [not null, default: 'active']
|
||||
command text [not null, default: '']
|
||||
sshKeyId text
|
||||
metricsConfig jsonb [not null, default: `{"server":{"type":"Remote","refreshRate":60,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
|
||||
}
|
||||
|
||||
table session {
|
||||
table session_temp {
|
||||
id text [pk, not null]
|
||||
expires_at timestamp [not null]
|
||||
token text [not null, unique]
|
||||
@@ -705,49 +900,49 @@ table "ssh-key" {
|
||||
description text
|
||||
createdAt text [not null]
|
||||
lastUsedAt text
|
||||
userId text
|
||||
organizationId text [not null]
|
||||
}
|
||||
|
||||
table telegram {
|
||||
telegramId text [pk, not null]
|
||||
botToken text [not null]
|
||||
chatId text [not null]
|
||||
messageThreadId text
|
||||
}
|
||||
|
||||
table user {
|
||||
table two_factor {
|
||||
id text [pk, not null]
|
||||
secret text [not null]
|
||||
backup_codes text [not null]
|
||||
user_id text [not null]
|
||||
}
|
||||
|
||||
table user_temp {
|
||||
id text [pk, not null]
|
||||
name text [not null, default: '']
|
||||
token text [not null]
|
||||
isRegistered boolean [not null, default: false]
|
||||
expirationDate text [not null]
|
||||
createdAt text [not null]
|
||||
canCreateProjects boolean [not null, default: false]
|
||||
canAccessToSSHKeys boolean [not null, default: false]
|
||||
canCreateServices boolean [not null, default: false]
|
||||
canDeleteProjects boolean [not null, default: false]
|
||||
canDeleteServices boolean [not null, default: false]
|
||||
canAccessToDocker boolean [not null, default: false]
|
||||
canAccessToAPI boolean [not null, default: false]
|
||||
canAccessToGitProviders boolean [not null, default: false]
|
||||
canAccessToTraefikFiles boolean [not null, default: false]
|
||||
accesedProjects text[] [not null, default: `ARRAY[]::text[]`]
|
||||
accesedServices text[] [not null, default: `ARRAY[]::text[]`]
|
||||
created_at timestamp [default: `now()`]
|
||||
two_factor_enabled boolean
|
||||
email text [not null, unique]
|
||||
email_verified boolean [not null]
|
||||
image text
|
||||
role text
|
||||
banned boolean
|
||||
ban_reason text
|
||||
ban_expires timestamp
|
||||
updated_at timestamp [not null]
|
||||
serverIp text
|
||||
certificateType certificateType [not null, default: 'none']
|
||||
https boolean [not null, default: false]
|
||||
host text
|
||||
letsEncryptEmail text
|
||||
sshPrivateKey text
|
||||
enableDockerCleanup boolean [not null, default: false]
|
||||
enableLogRotation boolean [not null, default: false]
|
||||
logCleanupCron text [default: '0 0 * * *']
|
||||
role text [not null, default: 'user']
|
||||
enablePaidFeatures boolean [not null, default: false]
|
||||
allowImpersonation boolean [not null, default: false]
|
||||
metricsConfig jsonb [not null, default: `{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
|
||||
cleanupCacheApplications boolean [not null, default: false]
|
||||
cleanupCacheOnPreviews boolean [not null, default: false]
|
||||
@@ -766,6 +961,29 @@ table verification {
|
||||
updated_at timestamp
|
||||
}
|
||||
|
||||
table volume_backup {
|
||||
volumeBackupId text [pk, not null]
|
||||
name text [not null]
|
||||
volumeName text [not null]
|
||||
prefix text [not null]
|
||||
serviceType serviceType [not null, default: 'application']
|
||||
appName text [not null]
|
||||
serviceName text
|
||||
turnOff boolean [not null, default: false]
|
||||
cronExpression text [not null]
|
||||
keepLatestCount integer
|
||||
enabled boolean
|
||||
applicationId text
|
||||
postgresId text
|
||||
mariadbId text
|
||||
mongoId text
|
||||
mysqlId text
|
||||
redisId text
|
||||
composeId text
|
||||
createdAt text [not null]
|
||||
destinationId text [not null]
|
||||
}
|
||||
|
||||
ref: mount.applicationId > application.applicationId
|
||||
|
||||
ref: mount.postgresId > postgres.postgresId
|
||||
@@ -780,7 +998,13 @@ ref: mount.redisId > redis.redisId
|
||||
|
||||
ref: mount.composeId > compose.composeId
|
||||
|
||||
ref: application.projectId > project.projectId
|
||||
ref: user_temp.id - account.user_id
|
||||
|
||||
ref: ai.organizationId - organization.id
|
||||
|
||||
ref: apikey.user_id > user_temp.id
|
||||
|
||||
ref: application.environmentId > environment.environmentId
|
||||
|
||||
ref: application.customGitSSHKeyId > "ssh-key".sshKeyId
|
||||
|
||||
@@ -790,6 +1014,8 @@ ref: application.githubId - github.githubId
|
||||
|
||||
ref: application.gitlabId - gitlab.gitlabId
|
||||
|
||||
ref: application.giteaId - gitea.giteaId
|
||||
|
||||
ref: application.bitbucketId - bitbucket.bitbucketId
|
||||
|
||||
ref: application.serverId > server.serverId
|
||||
@@ -804,13 +1030,17 @@ ref: backup.mysqlId > mysql.mysqlId
|
||||
|
||||
ref: backup.mongoId > mongo.mongoId
|
||||
|
||||
ref: backup.userId > user_temp.id
|
||||
|
||||
ref: backup.composeId > compose.composeId
|
||||
|
||||
ref: git_provider.gitProviderId - bitbucket.gitProviderId
|
||||
|
||||
ref: certificate.serverId > server.serverId
|
||||
|
||||
ref: certificate.userId - user.id
|
||||
ref: certificate.organizationId - organization.id
|
||||
|
||||
ref: compose.projectId > project.projectId
|
||||
ref: compose.environmentId > environment.environmentId
|
||||
|
||||
ref: compose.customGitSSHKeyId > "ssh-key".sshKeyId
|
||||
|
||||
@@ -820,6 +1050,8 @@ ref: compose.gitlabId - gitlab.gitlabId
|
||||
|
||||
ref: compose.bitbucketId - bitbucket.bitbucketId
|
||||
|
||||
ref: compose.giteaId - gitea.giteaId
|
||||
|
||||
ref: compose.serverId > server.serverId
|
||||
|
||||
ref: deployment.applicationId > application.applicationId
|
||||
@@ -830,7 +1062,15 @@ ref: deployment.serverId > server.serverId
|
||||
|
||||
ref: deployment.previewDeploymentId > preview_deployments.previewDeploymentId
|
||||
|
||||
ref: destination.userId - user.id
|
||||
ref: deployment.scheduleId > schedule.scheduleId
|
||||
|
||||
ref: deployment.backupId > backup.backupId
|
||||
|
||||
ref: rollback.deploymentId - deployment.deploymentId
|
||||
|
||||
ref: deployment.volumeBackupId > volume_backup.volumeBackupId
|
||||
|
||||
ref: destination.organizationId - organization.id
|
||||
|
||||
ref: domain.applicationId > application.applicationId
|
||||
|
||||
@@ -838,23 +1078,33 @@ ref: domain.composeId > compose.composeId
|
||||
|
||||
ref: preview_deployments.domainId - domain.domainId
|
||||
|
||||
ref: environment.projectId > project.projectId
|
||||
|
||||
ref: github.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitlab.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitea.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: git_provider.userId - user.id
|
||||
ref: git_provider.organizationId - organization.id
|
||||
|
||||
ref: mariadb.projectId > project.projectId
|
||||
ref: git_provider.userId - user_temp.id
|
||||
|
||||
ref: invitation.organization_id - organization.id
|
||||
|
||||
ref: mariadb.environmentId > environment.environmentId
|
||||
|
||||
ref: mariadb.serverId > server.serverId
|
||||
|
||||
ref: mongo.projectId > project.projectId
|
||||
ref: member.organization_id > organization.id
|
||||
|
||||
ref: member.user_id - user_temp.id
|
||||
|
||||
ref: mongo.environmentId > environment.environmentId
|
||||
|
||||
ref: mongo.serverId > server.serverId
|
||||
|
||||
ref: mysql.projectId > project.projectId
|
||||
ref: mysql.environmentId > environment.environmentId
|
||||
|
||||
ref: mysql.serverId > server.serverId
|
||||
|
||||
@@ -868,30 +1118,58 @@ ref: notification.emailId - email.emailId
|
||||
|
||||
ref: notification.gotifyId - gotify.gotifyId
|
||||
|
||||
ref: notification.userId - user.id
|
||||
ref: notification.ntfyId - ntfy.ntfyId
|
||||
|
||||
ref: notification.customId - custom.customId
|
||||
|
||||
ref: notification.organizationId - organization.id
|
||||
|
||||
ref: organization.owner_id > user_temp.id
|
||||
|
||||
ref: port.applicationId > application.applicationId
|
||||
|
||||
ref: postgres.projectId > project.projectId
|
||||
ref: postgres.environmentId > environment.environmentId
|
||||
|
||||
ref: postgres.serverId > server.serverId
|
||||
|
||||
ref: preview_deployments.applicationId > application.applicationId
|
||||
|
||||
ref: project.userId - user.id
|
||||
ref: project.organizationId > organization.id
|
||||
|
||||
ref: redirect.applicationId > application.applicationId
|
||||
|
||||
ref: redis.projectId > project.projectId
|
||||
ref: redis.environmentId > environment.environmentId
|
||||
|
||||
ref: redis.serverId > server.serverId
|
||||
|
||||
ref: registry.userId - user.id
|
||||
ref: schedule.applicationId - application.applicationId
|
||||
|
||||
ref: schedule.composeId > compose.composeId
|
||||
|
||||
ref: schedule.serverId > server.serverId
|
||||
|
||||
ref: schedule.userId > user_temp.id
|
||||
|
||||
ref: security.applicationId > application.applicationId
|
||||
|
||||
ref: server.userId - user.id
|
||||
|
||||
ref: server.sshKeyId > "ssh-key".sshKeyId
|
||||
|
||||
ref: "ssh-key".userId - user.id
|
||||
ref: server.organizationId > organization.id
|
||||
|
||||
ref: "ssh-key".organizationId - organization.id
|
||||
|
||||
ref: volume_backup.applicationId - application.applicationId
|
||||
|
||||
ref: volume_backup.postgresId - postgres.postgresId
|
||||
|
||||
ref: volume_backup.mariadbId - mariadb.mariadbId
|
||||
|
||||
ref: volume_backup.mongoId - mongo.mongoId
|
||||
|
||||
ref: volume_backup.mysqlId - mysql.mysqlId
|
||||
|
||||
ref: volume_backup.redisId - redis.redisId
|
||||
|
||||
ref: volume_backup.composeId - compose.composeId
|
||||
|
||||
ref: volume_backup.destinationId - destination.destinationId
|
||||
@@ -38,11 +38,9 @@ const createSchema = createInsertSchema(security, {
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneSecurity = createSchema
|
||||
.pick({
|
||||
securityId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneSecurity = z.object({
|
||||
securityId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateSecurity = createSchema
|
||||
.pick({
|
||||
|
||||
@@ -15,6 +15,7 @@ import { applications } from "./application";
|
||||
import { certificates } from "./certificate";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -24,6 +25,7 @@ import { schedules } from "./schedule";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
|
||||
export const serverType = pgEnum("serverType", ["deploy", "build"]);
|
||||
|
||||
export const server = pgTable("server", {
|
||||
serverId: text("serverId")
|
||||
@@ -44,6 +46,7 @@ export const server = pgTable("server", {
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
serverStatus: serverStatus("serverStatus").notNull().default("active"),
|
||||
serverType: serverType("serverType").notNull().default("deploy"),
|
||||
command: text("command").notNull().default(""),
|
||||
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
||||
onDelete: "set null",
|
||||
@@ -97,13 +100,24 @@ export const server = pgTable("server", {
|
||||
});
|
||||
|
||||
export const serverRelations = relations(server, ({ one, many }) => ({
|
||||
deployments: many(deployments),
|
||||
deployments: many(deployments, {
|
||||
relationName: "deploymentServer",
|
||||
}),
|
||||
buildDeployments: many(deployments, {
|
||||
relationName: "deploymentBuildServer",
|
||||
}),
|
||||
sshKey: one(sshKeys, {
|
||||
fields: [server.sshKeyId],
|
||||
references: [sshKeys.sshKeyId],
|
||||
}),
|
||||
applications: many(applications),
|
||||
applications: many(applications, {
|
||||
relationName: "applicationServer",
|
||||
}),
|
||||
buildApplications: many(applications, {
|
||||
relationName: "applicationBuildServer",
|
||||
}),
|
||||
compose: many(compose),
|
||||
libsql: many(libsql),
|
||||
redis: many(redis),
|
||||
mariadb: many(mariadb),
|
||||
mongo: many(mongo),
|
||||
@@ -121,6 +135,7 @@ const createSchema = createInsertSchema(server, {
|
||||
serverId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
serverType: z.enum(["deploy", "build"]).optional(),
|
||||
});
|
||||
|
||||
export const apiCreateServer = createSchema
|
||||
@@ -131,14 +146,13 @@ export const apiCreateServer = createSchema
|
||||
port: true,
|
||||
username: true,
|
||||
sshKeyId: true,
|
||||
serverType: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneServer = createSchema
|
||||
.pick({
|
||||
serverId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneServer = z.object({
|
||||
serverId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiRemoveServer = createSchema
|
||||
.pick({
|
||||
@@ -155,6 +169,7 @@ export const apiUpdateServer = createSchema
|
||||
port: true,
|
||||
username: true,
|
||||
sshKeyId: true,
|
||||
serverType: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { users_temp } from "./user";
|
||||
import { user } from "./user";
|
||||
|
||||
// OLD TABLE
|
||||
export const session = pgTable("session_temp", {
|
||||
export const session = pgTable("session", {
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
@@ -12,7 +12,7 @@ export const session = pgTable("session_temp", {
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
impersonatedBy: text("impersonated_by"),
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ export const certificateType = pgEnum("certificateType", [
|
||||
|
||||
export const triggerType = pgEnum("triggerType", ["push", "tag"]);
|
||||
|
||||
export const sqldNode = pgEnum("sqldNode", ["primary", "replica"]);
|
||||
|
||||
export interface HealthCheckSwarm {
|
||||
Test?: string[] | undefined;
|
||||
Interval?: number | undefined;
|
||||
@@ -74,6 +76,26 @@ export interface LabelsSwarm {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
export interface EndpointPortConfigSwarm {
|
||||
Protocol?: string | undefined;
|
||||
TargetPort?: number | undefined;
|
||||
PublishedPort?: number | undefined;
|
||||
PublishMode?: string | undefined;
|
||||
}
|
||||
|
||||
export interface EndpointSpecSwarm {
|
||||
Mode?: string | undefined;
|
||||
Ports?: EndpointPortConfigSwarm[] | undefined;
|
||||
}
|
||||
|
||||
export interface UlimitSwarm {
|
||||
Name: string;
|
||||
Soft: number;
|
||||
Hard: number;
|
||||
}
|
||||
|
||||
export type UlimitsSwarm = UlimitSwarm[];
|
||||
|
||||
export const HealthCheckSwarmSchema = z
|
||||
.object({
|
||||
Test: z.array(z.string()).optional(),
|
||||
@@ -155,9 +177,35 @@ export const NetworkSwarmSchema = z.array(
|
||||
.object({
|
||||
Target: z.string().optional(),
|
||||
Aliases: z.array(z.string()).optional(),
|
||||
DriverOpts: z.object({}).optional(),
|
||||
DriverOpts: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.strict(),
|
||||
);
|
||||
|
||||
export const LabelsSwarmSchema = z.record(z.string());
|
||||
export const LabelsSwarmSchema = z.record(z.string(), z.string());
|
||||
|
||||
export const EndpointPortConfigSwarmSchema = z
|
||||
.object({
|
||||
Protocol: z.string().optional(),
|
||||
TargetPort: z.number().optional(),
|
||||
PublishedPort: z.number().optional(),
|
||||
PublishMode: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const EndpointSpecSwarmSchema = z
|
||||
.object({
|
||||
Mode: z.string().optional(),
|
||||
Ports: z.array(EndpointPortConfigSwarmSchema).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const UlimitSwarmSchema = z
|
||||
.object({
|
||||
Name: z.string().min(1),
|
||||
Soft: z.number().int().min(-1),
|
||||
Hard: z.number().int().min(-1),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const UlimitsSwarmSchema = z.array(UlimitSwarmSchema);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { sshKeyCreate, sshKeyType } from "../validations";
|
||||
import { organization } from "./account";
|
||||
import { applications } from "./application";
|
||||
@@ -52,11 +53,9 @@ export const apiCreateSshKey = createSchema
|
||||
})
|
||||
.merge(sshKeyCreate.pick({ privateKey: true }));
|
||||
|
||||
export const apiFindOneSshKey = createSchema
|
||||
.pick({
|
||||
sshKeyId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneSshKey = z.object({
|
||||
sshKeyId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiGenerateSSHKey = sshKeyType;
|
||||
|
||||
|
||||
133
packages/server/src/db/schema/sso.ts
Normal file
133
packages/server/src/db/schema/sso.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { user } from "./user";
|
||||
|
||||
export const ssoProvider = pgTable("sso_provider", {
|
||||
id: text("id").primaryKey(),
|
||||
issuer: text("issuer").notNull(),
|
||||
oidcConfig: text("oidc_config"),
|
||||
samlConfig: text("saml_config"),
|
||||
providerId: text("provider_id").notNull().unique(),
|
||||
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
|
||||
organizationId: text("organization_id").references(() => organization.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
domain: text("domain").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [ssoProvider.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [ssoProvider.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/;
|
||||
export const ssoProviderBodySchema = z.object({
|
||||
providerId: z.string({}),
|
||||
issuer: z.string({}),
|
||||
domains: z
|
||||
.string()
|
||||
.array()
|
||||
.transform((val) =>
|
||||
Array.from(
|
||||
new Set(val.map((d) => d.trim().toLowerCase()).filter(Boolean)),
|
||||
),
|
||||
)
|
||||
.refine((val) => val.every((d) => domainRegex.test(d)), {
|
||||
message: "Invalid domain",
|
||||
path: ["domains"],
|
||||
}),
|
||||
oidcConfig: z
|
||||
.object({
|
||||
clientId: z.string({}),
|
||||
clientSecret: z.string({}),
|
||||
authorizationEndpoint: z.string({}).optional(),
|
||||
tokenEndpoint: z.string({}).optional(),
|
||||
userInfoEndpoint: z.string({}).optional(),
|
||||
tokenEndpointAuthentication: z
|
||||
.enum(["client_secret_post", "client_secret_basic"])
|
||||
.optional(),
|
||||
jwksEndpoint: z.string({}).optional(),
|
||||
discoveryEndpoint: z.string().optional(),
|
||||
skipDiscovery: z.boolean().optional(),
|
||||
scopes: z.array(z.string()).optional(),
|
||||
pkce: z.boolean().default(true).optional(),
|
||||
mapping: z
|
||||
.object({
|
||||
id: z.string({}),
|
||||
email: z.string({}),
|
||||
emailVerified: z.string({}).optional(),
|
||||
name: z.string({}),
|
||||
image: z.string({}).optional(),
|
||||
extraFields: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
samlConfig: z
|
||||
.object({
|
||||
entryPoint: z.string({}),
|
||||
cert: z.string({}),
|
||||
callbackUrl: z.string({}),
|
||||
audience: z.string().optional(),
|
||||
idpMetadata: z
|
||||
.object({
|
||||
metadata: z.string().optional(),
|
||||
entityID: z.string().optional(),
|
||||
cert: z.string().optional(),
|
||||
privateKey: z.string().optional(),
|
||||
privateKeyPass: z.string().optional(),
|
||||
isAssertionEncrypted: z.boolean().optional(),
|
||||
encPrivateKey: z.string().optional(),
|
||||
encPrivateKeyPass: z.string().optional(),
|
||||
singleSignOnService: z
|
||||
.array(
|
||||
z.object({
|
||||
Binding: z.string(),
|
||||
Location: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
spMetadata: z.object({
|
||||
metadata: z.string().optional(),
|
||||
entityID: z.string().optional(),
|
||||
binding: z.string().optional(),
|
||||
privateKey: z.string().optional(),
|
||||
privateKeyPass: z.string().optional(),
|
||||
isAssertionEncrypted: z.boolean().optional(),
|
||||
encPrivateKey: z.string().optional(),
|
||||
encPrivateKeyPass: z.string().optional(),
|
||||
}),
|
||||
wantAssertionsSigned: z.boolean().optional(),
|
||||
authnRequestsSigned: z.boolean().optional(),
|
||||
signatureAlgorithm: z.string().optional(),
|
||||
digestAlgorithm: z.string().optional(),
|
||||
identifierFormat: z.string().optional(),
|
||||
privateKey: z.string().optional(),
|
||||
decryptionPvk: z.string().optional(),
|
||||
additionalParams: z.record(z.string(), z.any()).optional(),
|
||||
mapping: z
|
||||
.object({
|
||||
id: z.string({}),
|
||||
email: z.string({}),
|
||||
emailVerified: z.string({}).optional(),
|
||||
name: z.string({}),
|
||||
firstName: z.string({}).optional(),
|
||||
lastName: z.string({}).optional(),
|
||||
extraFields: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
organizationId: z.string({}).optional(),
|
||||
overrideUserInfo: z.boolean({}).default(false).optional(),
|
||||
});
|
||||
99
packages/server/src/db/schema/tag.ts
Normal file
99
packages/server/src/db/schema/tag.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text, unique } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { projects } from "./project";
|
||||
|
||||
export const tags = pgTable(
|
||||
"tag",
|
||||
{
|
||||
tagId: text("tagId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
color: text("color"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => ({
|
||||
// Unique index on (organizationId, name) to prevent duplicate tag names per organization
|
||||
uniqueOrgName: unique("unique_org_tag_name").on(
|
||||
table.organizationId,
|
||||
table.name,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const projectTags = pgTable(
|
||||
"project_tag",
|
||||
{
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
tagId: text("tagId")
|
||||
.notNull()
|
||||
.references(() => tags.tagId, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => ({
|
||||
// Unique constraint to prevent duplicate project-tag associations
|
||||
uniqueProjectTag: unique("unique_project_tag").on(
|
||||
table.projectId,
|
||||
table.tagId,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const tagRelations = relations(tags, ({ one, many }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [tags.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
projectTags: many(projectTags),
|
||||
}));
|
||||
|
||||
export const projectTagRelations = relations(projectTags, ({ one }) => ({
|
||||
project: one(projects, {
|
||||
fields: [projectTags.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
tag: one(tags, {
|
||||
fields: [projectTags.tagId],
|
||||
references: [tags.tagId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(tags, {
|
||||
tagId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
color: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateTag = createSchema.pick({
|
||||
name: true,
|
||||
color: true,
|
||||
});
|
||||
|
||||
export const apiFindOneTag = z.object({
|
||||
tagId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiRemoveTag = createSchema
|
||||
.pick({
|
||||
tagId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateTag = createSchema.partial().extend({
|
||||
tagId: z.string().min(1),
|
||||
});
|
||||
@@ -3,7 +3,6 @@ import { relations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
@@ -15,7 +14,7 @@ import { account, apikey, organization } from "./account";
|
||||
import { backups } from "./backups";
|
||||
import { projects } from "./project";
|
||||
import { schedules } from "./schedule";
|
||||
import { certificateType } from "./shared";
|
||||
import { ssoProvider } from "./sso";
|
||||
/**
|
||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||
* database instance for multiple projects.
|
||||
@@ -26,12 +25,13 @@ import { certificateType } from "./shared";
|
||||
// OLD TABLE
|
||||
|
||||
// TEMP
|
||||
export const users_temp = pgTable("user_temp", {
|
||||
export const user = pgTable("user", {
|
||||
id: text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull().default(""),
|
||||
firstName: text("firstName").notNull().default(""),
|
||||
lastName: text("lastName").notNull().default(""),
|
||||
isRegistered: boolean("isRegistered").notNull().default(false),
|
||||
expirationDate: text("expirationDate")
|
||||
.notNull()
|
||||
@@ -50,95 +50,44 @@ export const users_temp = pgTable("user_temp", {
|
||||
banExpires: timestamp("ban_expires"),
|
||||
updatedAt: timestamp("updated_at").notNull(),
|
||||
// Admin
|
||||
serverIp: text("serverIp"),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
https: boolean("https").notNull().default(false),
|
||||
host: text("host"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
role: text("role").notNull().default("user"),
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
||||
metricsConfig: jsonb("metricsConfig")
|
||||
.$type<{
|
||||
server: {
|
||||
type: "Dokploy" | "Remote";
|
||||
refreshRate: number;
|
||||
port: number;
|
||||
token: string;
|
||||
urlCallback: string;
|
||||
retentionDays: number;
|
||||
cronJob: string;
|
||||
thresholds: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
};
|
||||
};
|
||||
containers: {
|
||||
refreshRate: number;
|
||||
services: {
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
};
|
||||
};
|
||||
}>()
|
||||
.notNull()
|
||||
.default({
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
refreshRate: 60,
|
||||
port: 4500,
|
||||
token: "",
|
||||
retentionDays: 2,
|
||||
cronJob: "",
|
||||
urlCallback: "",
|
||||
thresholds: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
},
|
||||
},
|
||||
containers: {
|
||||
refreshRate: 60,
|
||||
services: {
|
||||
include: [],
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
||||
// Enterprise / proprietary features
|
||||
enableEnterpriseFeatures: boolean("enableEnterpriseFeatures")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
||||
licenseKey: text("licenseKey"),
|
||||
isValidEnterpriseLicense: boolean("isValidEnterpriseLicense")
|
||||
.notNull()
|
||||
.default(false),
|
||||
stripeCustomerId: text("stripeCustomerId"),
|
||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||
trustedOrigins: text("trustedOrigins").array(),
|
||||
});
|
||||
|
||||
export const usersRelations = relations(users_temp, ({ one, many }) => ({
|
||||
export const usersRelations = relations(user, ({ one, many }) => ({
|
||||
account: one(account, {
|
||||
fields: [users_temp.id],
|
||||
fields: [user.id],
|
||||
references: [account.userId],
|
||||
}),
|
||||
organizations: many(organization),
|
||||
projects: many(projects),
|
||||
apiKeys: many(apikey),
|
||||
ssoProviders: many(ssoProvider),
|
||||
backups: many(backups),
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(users_temp, {
|
||||
const createSchema = createInsertSchema(user, {
|
||||
id: z.string().min(1),
|
||||
isRegistered: z.boolean().optional(),
|
||||
}).omit({
|
||||
role: true,
|
||||
trustedOrigins: true,
|
||||
isValidEnterpriseLicense: true,
|
||||
});
|
||||
|
||||
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
||||
@@ -186,6 +135,8 @@ export const apiAssignPermissions = createSchema
|
||||
canAccessToAPI: z.boolean().optional(),
|
||||
canAccessToSSHKeys: z.boolean().optional(),
|
||||
canAccessToGitProviders: z.boolean().optional(),
|
||||
canDeleteEnvironments: z.boolean().optional(),
|
||||
canCreateEnvironments: z.boolean().optional(),
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -200,33 +151,6 @@ export const apiFindOneUserByAuth = createSchema
|
||||
// authId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiSaveSSHKey = createSchema
|
||||
.pick({
|
||||
sshPrivateKey: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiAssignDomain = createSchema
|
||||
.pick({
|
||||
host: true,
|
||||
certificateType: true,
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
})
|
||||
.required()
|
||||
.partial({
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
});
|
||||
|
||||
export const apiUpdateDockerCleanup = createSchema
|
||||
.pick({
|
||||
enableDockerCleanup: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTraefikConfig = z.object({
|
||||
traefikConfig: z.string().min(1),
|
||||
@@ -295,32 +219,6 @@ export const apiReadStatsLogs = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateWebServerMonitoring = z.object({
|
||||
metricsConfig: z
|
||||
.object({
|
||||
server: z.object({
|
||||
refreshRate: z.number().min(2),
|
||||
port: z.number().min(1),
|
||||
token: z.string(),
|
||||
urlCallback: z.string().url(),
|
||||
retentionDays: z.number().min(1),
|
||||
cronJob: z.string().min(1),
|
||||
thresholds: z.object({
|
||||
cpu: z.number().min(0),
|
||||
memory: z.number().min(0),
|
||||
}),
|
||||
}),
|
||||
containers: z.object({
|
||||
refreshRate: z.number().min(2),
|
||||
services: z.object({
|
||||
include: z.array(z.string()).optional(),
|
||||
exclude: z.array(z.string()).optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export const apiUpdateUser = createSchema.partial().extend({
|
||||
email: z
|
||||
.string()
|
||||
@@ -329,30 +227,6 @@ export const apiUpdateUser = createSchema.partial().extend({
|
||||
.optional(),
|
||||
password: z.string().optional(),
|
||||
currentPassword: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
metricsConfig: z
|
||||
.object({
|
||||
server: z.object({
|
||||
type: z.enum(["Dokploy", "Remote"]),
|
||||
refreshRate: z.number(),
|
||||
port: z.number(),
|
||||
token: z.string(),
|
||||
urlCallback: z.string(),
|
||||
retentionDays: z.number(),
|
||||
cronJob: z.string(),
|
||||
thresholds: z.object({
|
||||
cpu: z.number(),
|
||||
memory: z.number(),
|
||||
}),
|
||||
}),
|
||||
containers: z.object({
|
||||
refreshRate: z.number(),
|
||||
services: z.object({
|
||||
include: z.array(z.string()),
|
||||
exclude: z.array(z.string()),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
logCleanupCron: z.string().optional().nullable(),
|
||||
firstName: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -6,6 +6,12 @@ const alphabet = "abcdefghijklmnopqrstuvwxyz123456789";
|
||||
|
||||
const customNanoid = customAlphabet(alphabet, 6);
|
||||
|
||||
/** App name: letters, numbers, dots, underscores, hyphens only (no spaces). Safe for shell/Docker. */
|
||||
export const APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
|
||||
|
||||
export const APP_NAME_MESSAGE =
|
||||
"App name can only contain letters, numbers, dots, underscores and hyphens";
|
||||
|
||||
export const generateAppName = (type: string) => {
|
||||
const verb = faker.hacker.verb().replace(/ /g, "-");
|
||||
const adjective = faker.hacker.adjective().replace(/ /g, "-");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { serviceType } from "./mount";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -7,9 +8,9 @@ import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { serviceType } from "./mount";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { redis } from "./redis";
|
||||
@@ -53,6 +54,9 @@ export const volumeBackups = pgTable("volume_backup", {
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -93,6 +97,10 @@ export const volumeBackupsRelations = relations(
|
||||
fields: [volumeBackups.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
libsql: one(libsql, {
|
||||
fields: [volumeBackups.libsqlId],
|
||||
references: [libsql.libsqlId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [volumeBackups.composeId],
|
||||
references: [compose.composeId],
|
||||
|
||||
238
packages/server/src/db/schema/web-server-settings.ts
Normal file
238
packages/server/src/db/schema/web-server-settings.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { certificateType } from "./shared";
|
||||
|
||||
export const webServerSettings = pgTable("webServerSettings", {
|
||||
id: text("id")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
// Web Server Configuration
|
||||
serverIp: text("serverIp"),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
https: boolean("https").notNull().default(false),
|
||||
host: text("host"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
// Metrics Configuration
|
||||
metricsConfig: jsonb("metricsConfig")
|
||||
.$type<{
|
||||
server: {
|
||||
type: "Dokploy" | "Remote";
|
||||
refreshRate: number;
|
||||
port: number;
|
||||
token: string;
|
||||
urlCallback: string;
|
||||
retentionDays: number;
|
||||
cronJob: string;
|
||||
thresholds: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
};
|
||||
};
|
||||
containers: {
|
||||
refreshRate: number;
|
||||
services: {
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
};
|
||||
};
|
||||
}>()
|
||||
.notNull()
|
||||
.default({
|
||||
server: {
|
||||
type: "Dokploy",
|
||||
refreshRate: 60,
|
||||
port: 4500,
|
||||
token: "",
|
||||
retentionDays: 2,
|
||||
cronJob: "",
|
||||
urlCallback: "",
|
||||
thresholds: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
},
|
||||
},
|
||||
containers: {
|
||||
refreshRate: 60,
|
||||
services: {
|
||||
include: [],
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Whitelabeling Configuration (Enterprise / Proprietary)
|
||||
whitelabelingConfig: jsonb("whitelabelingConfig")
|
||||
.$type<{
|
||||
appName: string | null;
|
||||
appDescription: string | null;
|
||||
logoUrl: string | null;
|
||||
faviconUrl: string | null;
|
||||
customCss: string | null;
|
||||
loginLogoUrl: string | null;
|
||||
supportUrl: string | null;
|
||||
docsUrl: string | null;
|
||||
errorPageTitle: string | null;
|
||||
errorPageDescription: string | null;
|
||||
metaTitle: string | null;
|
||||
footerText: string | null;
|
||||
}>()
|
||||
.default({
|
||||
appName: null,
|
||||
appDescription: null,
|
||||
logoUrl: null,
|
||||
faviconUrl: null,
|
||||
customCss: null,
|
||||
loginLogoUrl: null,
|
||||
supportUrl: null,
|
||||
docsUrl: null,
|
||||
errorPageTitle: null,
|
||||
errorPageDescription: null,
|
||||
metaTitle: null,
|
||||
footerText: null,
|
||||
}),
|
||||
// Cache Cleanup Configuration
|
||||
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
||||
.notNull()
|
||||
.default(false),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const webServerSettingsRelations = relations(
|
||||
webServerSettings,
|
||||
() => ({}),
|
||||
);
|
||||
|
||||
const createSchema = createInsertSchema(webServerSettings, {
|
||||
id: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateWebServerSettings = createSchema.partial().extend({
|
||||
serverIp: z.string().optional(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||
https: z.boolean().optional(),
|
||||
host: z.string().optional(),
|
||||
letsEncryptEmail: z.string().email().optional().nullable(),
|
||||
sshPrivateKey: z.string().optional(),
|
||||
enableDockerCleanup: z.boolean().optional(),
|
||||
logCleanupCron: z.string().optional().nullable(),
|
||||
metricsConfig: z
|
||||
.object({
|
||||
server: z.object({
|
||||
type: z.enum(["Dokploy", "Remote"]),
|
||||
refreshRate: z.number(),
|
||||
port: z.number(),
|
||||
token: z.string(),
|
||||
urlCallback: z.string(),
|
||||
retentionDays: z.number(),
|
||||
cronJob: z.string(),
|
||||
thresholds: z.object({
|
||||
cpu: z.number(),
|
||||
memory: z.number(),
|
||||
}),
|
||||
}),
|
||||
containers: z.object({
|
||||
refreshRate: z.number(),
|
||||
services: z.object({
|
||||
include: z.array(z.string()),
|
||||
exclude: z.array(z.string()),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
cleanupCacheApplications: z.boolean().optional(),
|
||||
cleanupCacheOnPreviews: z.boolean().optional(),
|
||||
cleanupCacheOnCompose: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiAssignDomain = z
|
||||
.object({
|
||||
host: z.string(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||
letsEncryptEmail: z
|
||||
.union([z.string().email(), z.literal("")])
|
||||
.optional()
|
||||
.nullable(),
|
||||
https: z.boolean().optional(),
|
||||
})
|
||||
.required()
|
||||
.partial({
|
||||
letsEncryptEmail: true,
|
||||
https: true,
|
||||
});
|
||||
|
||||
export const apiSaveSSHKey = z
|
||||
.object({
|
||||
sshPrivateKey: z.string(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateDockerCleanup = z.object({
|
||||
enableDockerCleanup: z.boolean(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
// Whitelabeling validation schemas
|
||||
const safeUrl = z
|
||||
.string()
|
||||
.refine((url) => /^https?:\/\//i.test(url), {
|
||||
message: "Only http:// and https:// URLs are allowed",
|
||||
})
|
||||
.nullable();
|
||||
|
||||
export const whitelabelingConfigSchema = z.object({
|
||||
appName: z.string().nullable(),
|
||||
appDescription: z.string().nullable(),
|
||||
logoUrl: safeUrl,
|
||||
faviconUrl: safeUrl,
|
||||
customCss: z.string().nullable(),
|
||||
loginLogoUrl: safeUrl,
|
||||
supportUrl: safeUrl,
|
||||
docsUrl: safeUrl,
|
||||
errorPageTitle: z.string().nullable(),
|
||||
errorPageDescription: z.string().nullable(),
|
||||
metaTitle: z.string().nullable(),
|
||||
footerText: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const apiUpdateWhitelabeling = z.object({
|
||||
whitelabelingConfig: whitelabelingConfigSchema,
|
||||
});
|
||||
|
||||
export const apiUpdateWebServerMonitoring = z.object({
|
||||
metricsConfig: z
|
||||
.object({
|
||||
server: z.object({
|
||||
refreshRate: z.number().min(2),
|
||||
port: z.number().min(1),
|
||||
token: z.string(),
|
||||
urlCallback: z.string().url(),
|
||||
retentionDays: z.number().min(1),
|
||||
cronJob: z.string().min(1),
|
||||
thresholds: z.object({
|
||||
cpu: z.number().min(0),
|
||||
memory: z.number().min(0),
|
||||
}),
|
||||
}),
|
||||
containers: z.object({
|
||||
refreshRate: z.number().min(2),
|
||||
services: z.object({
|
||||
include: z.array(z.string()).optional(),
|
||||
exclude: z.array(z.string()).optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
@@ -2,7 +2,13 @@ import { z } from "zod";
|
||||
|
||||
export const domain = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Add a hostname" }),
|
||||
host: z
|
||||
.string()
|
||||
.min(1, { message: "Add a hostname" })
|
||||
.refine((val) => val === val.trim(), {
|
||||
message: "Domain name cannot have leading or trailing spaces",
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
@@ -58,7 +64,13 @@ export const domain = z
|
||||
|
||||
export const domainCompose = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Host is required" }),
|
||||
host: z
|
||||
.string()
|
||||
.min(1, { message: "Add a hostname" })
|
||||
.refine((val) => val === val.trim(), {
|
||||
message: "Domain name cannot have leading or trailing spaces",
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
|
||||
@@ -19,6 +19,7 @@ export type TemplateProps = {
|
||||
applicationType: string;
|
||||
buildLink: string;
|
||||
date: string;
|
||||
environmentName: string;
|
||||
};
|
||||
|
||||
export const BuildSuccessEmail = ({
|
||||
@@ -27,6 +28,7 @@ export const BuildSuccessEmail = ({
|
||||
applicationType = "application",
|
||||
buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
|
||||
date = "2023-05-01T00:00:00.000Z",
|
||||
environmentName = "production",
|
||||
}: TemplateProps) => {
|
||||
const previewText = `Build success for ${applicationName}`;
|
||||
return (
|
||||
@@ -74,6 +76,9 @@ export const BuildSuccessEmail = ({
|
||||
<Text className="!leading-3">
|
||||
Application Name: <strong>{applicationName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Environment: <strong>{environmentName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Application Type: <strong>{applicationType}</strong>
|
||||
</Text>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
export type TemplateProps = {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
date: string;
|
||||
|
||||
124
packages/server/src/emails/emails/volume-backup.tsx
Normal file
124
packages/server/src/emails/emails/volume-backup.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
export type TemplateProps = {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
volumeName: string;
|
||||
serviceType:
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose"
|
||||
| "libsql";
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
backupSize?: string;
|
||||
date: string;
|
||||
};
|
||||
|
||||
export const VolumeBackupEmail = ({
|
||||
projectName = "dokploy",
|
||||
applicationName = "frontend",
|
||||
volumeName = "app-data",
|
||||
serviceType = "application",
|
||||
type = "success",
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date = "2023-05-01T00:00:00.000Z",
|
||||
}: TemplateProps) => {
|
||||
const previewText = `Volume backup for ${applicationName} was ${type === "success" ? "successful ✅" : "failed ❌"}`;
|
||||
return (
|
||||
<Html>
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#007291",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Head />
|
||||
|
||||
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
alt="Dokploy"
|
||||
className="my-0 mx-auto"
|
||||
/>
|
||||
</Section>
|
||||
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||
Volume backup for <strong>{applicationName}</strong>
|
||||
</Heading>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
Hello,
|
||||
</Text>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
Your volume backup for <strong>{applicationName}</strong> was{" "}
|
||||
{type === "success"
|
||||
? "successful ✅"
|
||||
: "failed. Please check the error message below. ❌"}
|
||||
.
|
||||
</Text>
|
||||
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||
<Text className="!leading-3 font-bold">Details: </Text>
|
||||
<Text className="!leading-3">
|
||||
Project Name: <strong>{projectName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Application Name: <strong>{applicationName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Volume Name: <strong>{volumeName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Service Type: <strong>{serviceType}</strong>
|
||||
</Text>
|
||||
{backupSize && (
|
||||
<Text className="!leading-3">
|
||||
Backup Size: <strong>{backupSize}</strong>
|
||||
</Text>
|
||||
)}
|
||||
<Text className="!leading-3">
|
||||
Date: <strong>{date}</strong>
|
||||
</Text>
|
||||
</Section>
|
||||
{type === "error" && errorMessage ? (
|
||||
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||
<Text className="!leading-3 font-bold">Reason: </Text>
|
||||
<Text className="text-[12px] leading-[24px]">
|
||||
{errorMessage || "Error message not provided"}
|
||||
</Text>
|
||||
</Section>
|
||||
) : null}
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default VolumeBackupEmail;
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./auth/random-password";
|
||||
export * from "./constants/index";
|
||||
export * from "./db/constants";
|
||||
export * from "./db/validations/domain";
|
||||
export * from "./db/validations/index";
|
||||
export * from "./lib/auth";
|
||||
@@ -21,15 +22,20 @@ export * from "./services/git-provider";
|
||||
export * from "./services/gitea";
|
||||
export * from "./services/github";
|
||||
export * from "./services/gitlab";
|
||||
export * from "./services/libsql";
|
||||
export * from "./services/mariadb";
|
||||
export * from "./services/mongo";
|
||||
export * from "./services/mount";
|
||||
export * from "./services/mysql";
|
||||
export * from "./services/notification";
|
||||
export * from "./services/patch";
|
||||
export * from "./services/patch-repo";
|
||||
export * from "./services/port";
|
||||
export * from "./services/postgres";
|
||||
export * from "./services/preview-deployment";
|
||||
export * from "./services/project";
|
||||
export * from "./services/proprietary/license-key";
|
||||
export * from "./services/proprietary/sso";
|
||||
export * from "./services/redirect";
|
||||
export * from "./services/redis";
|
||||
export * from "./services/registry";
|
||||
@@ -41,6 +47,7 @@ export * from "./services/settings";
|
||||
export * from "./services/ssh-key";
|
||||
export * from "./services/user";
|
||||
export * from "./services/volume-backups";
|
||||
export * from "./services/web-server-settings";
|
||||
export * from "./setup/config-paths";
|
||||
export * from "./setup/monitoring-setup";
|
||||
export * from "./setup/postgres-setup";
|
||||
@@ -61,6 +68,7 @@ export * from "./utils/access-log/types";
|
||||
export * from "./utils/access-log/utils";
|
||||
export * from "./utils/backups/compose";
|
||||
export * from "./utils/backups/index";
|
||||
export * from "./utils/backups/libsql";
|
||||
export * from "./utils/backups/mariadb";
|
||||
export * from "./utils/backups/mongo";
|
||||
export * from "./utils/backups/mysql";
|
||||
@@ -76,8 +84,8 @@ export * from "./utils/builders/nixpacks";
|
||||
export * from "./utils/builders/paketo";
|
||||
export * from "./utils/builders/static";
|
||||
export * from "./utils/builders/utils";
|
||||
|
||||
export * from "./utils/cluster/upload";
|
||||
export * from "./utils/crons/enterprise";
|
||||
export * from "./utils/databases/rebuild";
|
||||
export * from "./utils/docker/collision";
|
||||
export * from "./utils/docker/compose";
|
||||
@@ -112,6 +120,8 @@ export * from "./utils/providers/raw";
|
||||
export * from "./utils/schedules/index";
|
||||
export * from "./utils/schedules/utils";
|
||||
export * from "./utils/servers/remote-docker";
|
||||
export * from "./utils/startup/cancell-deployments";
|
||||
export * from "./utils/tracking/hubspot";
|
||||
export * from "./utils/traefik/application";
|
||||
export * from "./utils/traefik/domain";
|
||||
export * from "./utils/traefik/file-types";
|
||||
|
||||
195
packages/server/src/lib/access-control.ts
Normal file
195
packages/server/src/lib/access-control.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { createAccessControl } from "better-auth/plugins/access";
|
||||
|
||||
/**
|
||||
* Dokploy Access Control Statements
|
||||
*
|
||||
* Defines all resources and their possible actions across the platform.
|
||||
* The first 5 (organization, member, invitation, team, ac) are better-auth defaults
|
||||
* used internally by the organization plugin.
|
||||
* The rest are Dokploy-specific resources.
|
||||
*
|
||||
* Enterprise-only resources (only assignable via custom roles):
|
||||
* deployment, envVars, server, registry, certificate, backup, domain, logs, monitoring
|
||||
*/
|
||||
export const statements = {
|
||||
// better-auth organization plugin defaults
|
||||
organization: ["update", "delete"],
|
||||
member: ["read", "create", "update", "delete"],
|
||||
invitation: ["create", "cancel"],
|
||||
team: ["create", "update", "delete"],
|
||||
ac: ["create", "read", "update", "delete"],
|
||||
|
||||
// Dokploy core resources (free tier)
|
||||
project: ["create", "delete"],
|
||||
service: ["create", "read", "delete"],
|
||||
environment: ["create", "read", "delete"],
|
||||
docker: ["read"],
|
||||
sshKeys: ["read", "create", "delete"],
|
||||
gitProviders: ["read", "create", "delete"],
|
||||
traefikFiles: ["read", "write"],
|
||||
api: ["read"],
|
||||
|
||||
// Enterprise-only resources (custom roles only)
|
||||
volume: ["read", "create", "delete"],
|
||||
deployment: ["read", "create", "cancel"],
|
||||
envVars: ["read", "write"],
|
||||
projectEnvVars: ["read", "write"],
|
||||
environmentEnvVars: ["read", "write"],
|
||||
server: ["read", "create", "delete"],
|
||||
registry: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "delete"],
|
||||
backup: ["read", "create", "update", "delete", "restore"],
|
||||
volumeBackup: ["read", "create", "update", "delete", "restore"],
|
||||
schedule: ["read", "create", "update", "delete"],
|
||||
domain: ["read", "create", "delete"],
|
||||
destination: ["read", "create", "delete"],
|
||||
notification: ["read", "create", "update", "delete"],
|
||||
tag: ["read", "create", "update", "delete"],
|
||||
logs: ["read"],
|
||||
monitoring: ["read"],
|
||||
auditLog: ["read"],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Enterprise-only resources. For static roles (owner/admin/member),
|
||||
* permission checks on these resources are bypassed — they only apply
|
||||
* when using custom roles with an enterprise license.
|
||||
*/
|
||||
export const enterpriseOnlyResources = new Set<string>([
|
||||
"volume",
|
||||
"deployment",
|
||||
"envVars",
|
||||
"projectEnvVars",
|
||||
"environmentEnvVars",
|
||||
"server",
|
||||
"registry",
|
||||
"certificate",
|
||||
"backup",
|
||||
"volumeBackup",
|
||||
"schedule",
|
||||
"domain",
|
||||
"destination",
|
||||
"notification",
|
||||
"tag",
|
||||
"logs",
|
||||
"monitoring",
|
||||
"auditLog",
|
||||
]);
|
||||
|
||||
export const ac = createAccessControl(statements);
|
||||
|
||||
/**
|
||||
* Owner role — full access to everything
|
||||
*/
|
||||
export const ownerRole = ac.newRole({
|
||||
organization: ["update", "delete"],
|
||||
member: ["read", "create", "update", "delete"],
|
||||
invitation: ["create", "cancel"],
|
||||
team: ["create", "update", "delete"],
|
||||
ac: ["create", "read", "update", "delete"],
|
||||
project: ["create", "delete"],
|
||||
service: ["create", "read", "delete"],
|
||||
environment: ["create", "read", "delete"],
|
||||
docker: ["read"],
|
||||
sshKeys: ["read", "create", "delete"],
|
||||
gitProviders: ["read", "create", "delete"],
|
||||
traefikFiles: ["read", "write"],
|
||||
api: ["read"],
|
||||
volume: ["read", "create", "delete"],
|
||||
deployment: ["read", "create", "cancel"],
|
||||
envVars: ["read", "write"],
|
||||
projectEnvVars: ["read", "write"],
|
||||
environmentEnvVars: ["read", "write"],
|
||||
server: ["read", "create", "delete"],
|
||||
registry: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "delete"],
|
||||
backup: ["read", "create", "update", "delete", "restore"],
|
||||
volumeBackup: ["read", "create", "update", "delete", "restore"],
|
||||
schedule: ["read", "create", "update", "delete"],
|
||||
domain: ["read", "create", "delete"],
|
||||
destination: ["read", "create", "delete"],
|
||||
notification: ["read", "create", "update", "delete"],
|
||||
tag: ["read", "create", "update", "delete"],
|
||||
logs: ["read"],
|
||||
monitoring: ["read"],
|
||||
auditLog: ["read"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Admin role — same as owner but cannot delete the organization
|
||||
*/
|
||||
export const adminRole = ac.newRole({
|
||||
organization: ["update"],
|
||||
member: ["read", "create", "update", "delete"],
|
||||
invitation: ["create", "cancel"],
|
||||
team: ["create", "update", "delete"],
|
||||
ac: ["create", "read", "update", "delete"],
|
||||
project: ["create", "delete"],
|
||||
service: ["create", "read", "delete"],
|
||||
environment: ["create", "read", "delete"],
|
||||
docker: ["read"],
|
||||
sshKeys: ["read", "create", "delete"],
|
||||
gitProviders: ["read", "create", "delete"],
|
||||
traefikFiles: ["read", "write"],
|
||||
api: ["read"],
|
||||
volume: ["read", "create", "delete"],
|
||||
deployment: ["read", "create", "cancel"],
|
||||
envVars: ["read", "write"],
|
||||
projectEnvVars: ["read", "write"],
|
||||
environmentEnvVars: ["read", "write"],
|
||||
server: ["read", "create", "delete"],
|
||||
registry: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "delete"],
|
||||
backup: ["read", "create", "update", "delete", "restore"],
|
||||
volumeBackup: ["read", "create", "update", "delete", "restore"],
|
||||
schedule: ["read", "create", "update", "delete"],
|
||||
domain: ["read", "create", "delete"],
|
||||
destination: ["read", "create", "delete"],
|
||||
notification: ["read", "create", "update", "delete"],
|
||||
tag: ["read", "create", "update", "delete"],
|
||||
logs: ["read"],
|
||||
monitoring: ["read"],
|
||||
auditLog: ["read"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Member role (free tier) — read-only base permissions.
|
||||
* Members can read projects/services/environments they have access to,
|
||||
* but cannot create, delete, or access admin resources.
|
||||
* Enterprise resources are not available to the base member role.
|
||||
*/
|
||||
export const memberRole = ac.newRole({
|
||||
organization: [],
|
||||
member: [],
|
||||
invitation: [],
|
||||
team: [],
|
||||
ac: ["read"],
|
||||
project: [],
|
||||
service: ["read"],
|
||||
environment: ["read"],
|
||||
docker: [],
|
||||
sshKeys: [],
|
||||
gitProviders: [],
|
||||
traefikFiles: [],
|
||||
api: [],
|
||||
// Service-level enterprise resources — member can do everything within services they have access to
|
||||
volume: ["read", "create", "delete"],
|
||||
deployment: ["read", "create", "cancel"],
|
||||
envVars: ["read", "write"],
|
||||
projectEnvVars: ["read", "write"],
|
||||
environmentEnvVars: ["read", "write"],
|
||||
backup: ["read", "create", "update", "delete", "restore"],
|
||||
volumeBackup: ["read", "create", "update", "delete", "restore"],
|
||||
schedule: ["read", "create", "update", "delete"],
|
||||
domain: ["read", "create", "delete"],
|
||||
logs: ["read"],
|
||||
monitoring: ["read"],
|
||||
// Org-level enterprise resources — member cannot manage these
|
||||
server: [],
|
||||
registry: [],
|
||||
certificate: [],
|
||||
destination: [],
|
||||
notification: [],
|
||||
tag: ["read"],
|
||||
auditLog: [],
|
||||
});
|
||||
@@ -1,23 +1,66 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { apiKey } from "@better-auth/api-key";
|
||||
import { sso } from "@better-auth/sso";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { admin, apiKey, organization, twoFactor } from "better-auth/plugins";
|
||||
import { admin, organization, twoFactor } from "better-auth/plugins";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { BETTER_AUTH_SECRET, IS_CLOUD } from "../constants";
|
||||
import { db } from "../db";
|
||||
import * as schema from "../db/schema";
|
||||
import { getUserByToken } from "../services/admin";
|
||||
import { updateUser } from "../services/user";
|
||||
import {
|
||||
getTrustedOrigins,
|
||||
getTrustedProviders,
|
||||
getUserByToken,
|
||||
} from "../services/admin";
|
||||
import { createAuditLog } from "../services/proprietary/audit-log";
|
||||
import {
|
||||
getWebServerSettings,
|
||||
updateWebServerSettings,
|
||||
} from "../services/web-server-settings";
|
||||
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
|
||||
import { sendEmail } from "../verification/send-verification-email";
|
||||
import { getPublicIpWithFallback } from "../wss/utils";
|
||||
import { ac, adminRole, memberRole, ownerRole } from "./access-control";
|
||||
|
||||
const { handler, api } = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema: schema,
|
||||
}),
|
||||
disabledPaths: [
|
||||
"/sso/register",
|
||||
"/organization/create",
|
||||
"/organization/update",
|
||||
"/organization/delete",
|
||||
],
|
||||
secret: BETTER_AUTH_SECRET,
|
||||
...(!IS_CLOUD
|
||||
? {
|
||||
advanced: {
|
||||
useSecureCookies: false,
|
||||
defaultCookieAttributes: {
|
||||
sameSite: "lax",
|
||||
secure: false,
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
account: {
|
||||
accountLinking: {
|
||||
enabled: true,
|
||||
async trustedProviders() {
|
||||
const fromDb = await getTrustedProviders();
|
||||
return ["github", "google", ...fromDb];
|
||||
},
|
||||
allowDifferentEmails: true,
|
||||
},
|
||||
},
|
||||
appName: "Dokploy",
|
||||
socialProviders: {
|
||||
github: {
|
||||
@@ -32,26 +75,34 @@ const { handler, api } = betterAuth({
|
||||
logger: {
|
||||
disabled: process.env.NODE_ENV === "production",
|
||||
},
|
||||
...(!IS_CLOUD && {
|
||||
async trustedOrigins() {
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (admin) {
|
||||
return [
|
||||
...(admin.user.serverIp
|
||||
? [`http://${admin.user.serverIp}:3000`]
|
||||
: []),
|
||||
...(admin.user.host ? [`https://${admin.user.host}`] : []),
|
||||
];
|
||||
async trustedOrigins() {
|
||||
try {
|
||||
if (IS_CLOUD) {
|
||||
return await getTrustedOrigins();
|
||||
}
|
||||
const [trustedOrigins, settings] = await Promise.all([
|
||||
getTrustedOrigins(),
|
||||
getWebServerSettings(),
|
||||
]);
|
||||
if (!settings) return [];
|
||||
const devOrigins =
|
||||
process.env.NODE_ENV === "development"
|
||||
? [
|
||||
"http://localhost:3000",
|
||||
"https://absolutely-handy-falcon.ngrok-free.app",
|
||||
]
|
||||
: [];
|
||||
return [
|
||||
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
||||
...(settings?.host ? [`https://${settings?.host}`] : []),
|
||||
...devOrigins,
|
||||
...trustedOrigins,
|
||||
];
|
||||
} catch (error) {
|
||||
console.error("Failed to resolve trusted origins:", error);
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
autoSignInAfterVerification: true,
|
||||
@@ -70,7 +121,7 @@ const { handler, api } = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
autoSignIn: !IS_CLOUD,
|
||||
requireEmailVerification: IS_CLOUD,
|
||||
requireEmailVerification: IS_CLOUD && process.env.NODE_ENV === "production",
|
||||
password: {
|
||||
async hash(password) {
|
||||
return bcrypt.hashSync(password, 10);
|
||||
@@ -104,6 +155,10 @@ const { handler, api } = betterAuth({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const isSSORequest = context?.path.includes("/sso");
|
||||
if (isSSORequest) {
|
||||
return;
|
||||
}
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
});
|
||||
@@ -115,17 +170,43 @@ const { handler, api } = betterAuth({
|
||||
}
|
||||
}
|
||||
},
|
||||
after: async (user) => {
|
||||
after: async (user, context) => {
|
||||
const isSSORequest = context?.path.includes("/sso");
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
});
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
await updateUser(user.id, {
|
||||
await updateWebServerSettings({
|
||||
serverIp: await getPublicIpWithFallback(),
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_CLOUD) {
|
||||
try {
|
||||
const hutk = getHubSpotUTK(
|
||||
context?.request?.headers?.get("cookie") || undefined,
|
||||
);
|
||||
// Cast to include additional fields
|
||||
const userWithFields = user as typeof user & {
|
||||
lastName?: string;
|
||||
};
|
||||
const hubspotSuccess = await submitToHubSpot(
|
||||
{
|
||||
email: user.email,
|
||||
firstName: user.name || "", // name is mapped to firstName column
|
||||
lastName: userWithFields.lastName || "",
|
||||
},
|
||||
hutk,
|
||||
);
|
||||
if (!hubspotSuccess) {
|
||||
console.error("Failed to submit to HubSpot");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting to HubSpot", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_CLOUD || !isAdminPresent) {
|
||||
await db.transaction(async (tx) => {
|
||||
const organization = await tx
|
||||
@@ -143,8 +224,32 @@ const { handler, api } = betterAuth({
|
||||
organizationId: organization?.id || "",
|
||||
role: "owner",
|
||||
createdAt: new Date(),
|
||||
isDefault: true, // Mark first organization as default
|
||||
});
|
||||
});
|
||||
} else if (isSSORequest) {
|
||||
const providerId = context?.params?.providerId;
|
||||
if (!providerId) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Provider ID is required",
|
||||
});
|
||||
}
|
||||
const provider = await db.query.ssoProvider.findFirst({
|
||||
where: eq(schema.ssoProvider.providerId, providerId),
|
||||
});
|
||||
|
||||
if (!provider) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Provider not found",
|
||||
});
|
||||
}
|
||||
await db.insert(schema.member).values({
|
||||
userId: user.id,
|
||||
organizationId: provider?.organizationId || "",
|
||||
role: "member",
|
||||
createdAt: new Date(),
|
||||
isDefault: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -152,9 +257,14 @@ const { handler, api } = betterAuth({
|
||||
session: {
|
||||
create: {
|
||||
before: async (session) => {
|
||||
// Find the default organization for this user
|
||||
// Priority: 1) isDefault=true, 2) most recently created
|
||||
const member = await db.query.member.findFirst({
|
||||
where: eq(schema.member.userId, session.userId),
|
||||
orderBy: desc(schema.member.createdAt),
|
||||
orderBy: [
|
||||
desc(schema.member.isDefault),
|
||||
desc(schema.member.createdAt),
|
||||
],
|
||||
with: {
|
||||
organization: true,
|
||||
},
|
||||
@@ -167,6 +277,52 @@ const { handler, api } = betterAuth({
|
||||
},
|
||||
};
|
||||
},
|
||||
after: async (session) => {
|
||||
const orgId = (
|
||||
session as typeof session & { activeOrganizationId?: string }
|
||||
).activeOrganizationId;
|
||||
if (!orgId) return;
|
||||
const memberRecord = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(schema.member.userId, session.userId),
|
||||
eq(schema.member.organizationId, orgId),
|
||||
),
|
||||
with: { user: true },
|
||||
});
|
||||
if (!memberRecord) return;
|
||||
await createAuditLog({
|
||||
organizationId: orgId,
|
||||
userId: session.userId,
|
||||
userEmail: memberRecord.user.email,
|
||||
userRole: memberRecord.role,
|
||||
action: "login",
|
||||
resourceType: "session",
|
||||
});
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
after: async (session) => {
|
||||
const orgId = (
|
||||
session as typeof session & { activeOrganizationId?: string }
|
||||
).activeOrganizationId;
|
||||
if (!orgId) return;
|
||||
const memberRecord = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(schema.member.userId, session.userId),
|
||||
eq(schema.member.organizationId, orgId),
|
||||
),
|
||||
with: { user: true },
|
||||
});
|
||||
if (!memberRecord) return;
|
||||
await createAuditLog({
|
||||
organizationId: orgId,
|
||||
userId: session.userId,
|
||||
userEmail: memberRecord.user.email,
|
||||
userRole: memberRecord.role,
|
||||
action: "logout",
|
||||
resourceType: "session",
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -175,7 +331,10 @@ const { handler, api } = betterAuth({
|
||||
updateAge: 60 * 60 * 24,
|
||||
},
|
||||
user: {
|
||||
modelName: "users_temp",
|
||||
modelName: "user",
|
||||
fields: {
|
||||
name: "firstName", // Map better-auth's default 'name' field to 'firstName' column
|
||||
},
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: "string",
|
||||
@@ -192,14 +351,42 @@ const { handler, api } = betterAuth({
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
},
|
||||
lastName: {
|
||||
type: "string",
|
||||
required: false,
|
||||
input: true,
|
||||
defaultValue: "",
|
||||
},
|
||||
enableEnterpriseFeatures: {
|
||||
type: "boolean",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
isValidEnterpriseLicense: {
|
||||
type: "boolean",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
apiKey({
|
||||
enableMetadata: true,
|
||||
references: "user",
|
||||
}),
|
||||
sso(),
|
||||
twoFactor(),
|
||||
organization({
|
||||
ac,
|
||||
roles: {
|
||||
owner: ownerRole,
|
||||
admin: adminRole,
|
||||
member: memberRole,
|
||||
},
|
||||
dynamicAccessControl: {
|
||||
enabled: true,
|
||||
maximumRolesPerOrganization: 10,
|
||||
},
|
||||
async sendInvitationEmail(data, _request) {
|
||||
if (IS_CLOUD) {
|
||||
const host =
|
||||
@@ -228,11 +415,16 @@ const { handler, api } = betterAuth({
|
||||
],
|
||||
});
|
||||
|
||||
export const auth = {
|
||||
const _auth = {
|
||||
handler,
|
||||
createApiKey: api.createApiKey,
|
||||
registerSSOProvider: api.registerSSOProvider,
|
||||
updateSSOProvider: api.updateSSOProvider,
|
||||
};
|
||||
|
||||
export type AuthType = typeof _auth;
|
||||
export const auth: AuthType = _auth;
|
||||
|
||||
export const validateRequest = async (request: IncomingMessage) => {
|
||||
const apiKey = request.headers["x-api-key"] as string;
|
||||
if (apiKey) {
|
||||
@@ -244,7 +436,7 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || "Error verifying API key");
|
||||
throw new Error(error.message?.toString() || "Error verifying API key");
|
||||
}
|
||||
if (!valid || !key) {
|
||||
return {
|
||||
@@ -288,16 +480,11 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
emailVerified,
|
||||
image,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
twoFactorEnabled,
|
||||
} = apiKeyRecord.user;
|
||||
// When accessing from DB, use actual column names
|
||||
const userFromDb = apiKeyRecord.user as typeof apiKeyRecord.user & {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
|
||||
const mockSession = {
|
||||
session: {
|
||||
@@ -305,16 +492,18 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
activeOrganizationId: organizationId || "",
|
||||
},
|
||||
user: {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
emailVerified,
|
||||
image,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
twoFactorEnabled,
|
||||
id: userFromDb.id,
|
||||
name: userFromDb.firstName, // Map firstName back to name for better-auth
|
||||
email: userFromDb.email,
|
||||
emailVerified: userFromDb.emailVerified,
|
||||
image: userFromDb.image,
|
||||
createdAt: userFromDb.createdAt,
|
||||
updatedAt: userFromDb.updatedAt,
|
||||
twoFactorEnabled: userFromDb.twoFactorEnabled,
|
||||
role: member?.role || "member",
|
||||
ownerId: member?.organization.ownerId || apiKeyRecord.user.id,
|
||||
enableEnterpriseFeatures: userFromDb.enableEnterpriseFeatures,
|
||||
isValidEnterpriseLicense: userFromDb.isValidEnterpriseLicense,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -346,17 +535,28 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
const member = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(schema.member.userId, session.user.id),
|
||||
eq(
|
||||
schema.member.organizationId,
|
||||
session.session.activeOrganizationId || "",
|
||||
),
|
||||
...(session.session.activeOrganizationId
|
||||
? [
|
||||
eq(
|
||||
schema.member.organizationId,
|
||||
session.session.activeOrganizationId || "",
|
||||
),
|
||||
]
|
||||
: []),
|
||||
),
|
||||
orderBy: [desc(schema.member.isDefault), desc(schema.member.createdAt)],
|
||||
with: {
|
||||
organization: true,
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
session.user.role = member?.role || "member";
|
||||
session.user.enableEnterpriseFeatures =
|
||||
member?.user.enableEnterpriseFeatures || false;
|
||||
session.user.isValidEnterpriseLicense =
|
||||
member?.user.isValidEnterpriseLicense || false;
|
||||
session.session.activeOrganizationId = member?.organization.id || "";
|
||||
if (member) {
|
||||
session.user.ownerId = member.organization.ownerId;
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { promises } from "node:fs";
|
||||
import osUtils from "node-os-utils";
|
||||
import { OSUtils } from "node-os-utils";
|
||||
import { paths } from "../constants";
|
||||
|
||||
export interface Container {
|
||||
@@ -38,22 +38,122 @@ export const recordAdvancedStats = async (
|
||||
});
|
||||
|
||||
if (appName === "dokploy") {
|
||||
const disk = await osUtils.drive.info("/");
|
||||
const osutils = new OSUtils();
|
||||
const diskResult = await osutils.disk.usageByMountPoint("/");
|
||||
|
||||
const diskUsage = disk.usedGb;
|
||||
const diskTotal = disk.totalGb;
|
||||
const diskUsedPercentage = disk.usedPercentage;
|
||||
const diskFree = disk.freeGb;
|
||||
if (diskResult.success && diskResult.data) {
|
||||
const disk = diskResult.data;
|
||||
const diskUsage = disk.used.toGB().toFixed(2);
|
||||
const diskTotal = disk.total.toGB().toFixed(2);
|
||||
const diskUsedPercentage = disk.usagePercentage;
|
||||
const diskFree = disk.available.toGB().toFixed(2);
|
||||
|
||||
await updateStatsFile(appName, "disk", {
|
||||
diskTotal: +diskTotal,
|
||||
diskUsedPercentage: +diskUsedPercentage,
|
||||
diskUsage: +diskUsage,
|
||||
diskFree: +diskFree,
|
||||
});
|
||||
await updateStatsFile(appName, "disk", {
|
||||
diskTotal: +diskTotal,
|
||||
diskUsedPercentage: +diskUsedPercentage,
|
||||
diskUsage: +diskUsage,
|
||||
diskFree: +diskFree,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get host system statistics using node-os-utils
|
||||
* This is used when monitoring "dokploy" to show host stats instead of container stats
|
||||
*/
|
||||
export const getHostSystemStats = async (): Promise<Container> => {
|
||||
const osutils = new OSUtils({
|
||||
disk: {
|
||||
includeStats: true, // Enable disk I/O statistics
|
||||
},
|
||||
});
|
||||
|
||||
// Get CPU usage
|
||||
const cpuResult = await osutils.cpu.usage();
|
||||
const cpuUsage = cpuResult.success ? cpuResult.data : 0;
|
||||
|
||||
// Get memory info
|
||||
const memResult = await osutils.memory.info();
|
||||
let memUsedGB = 0;
|
||||
let memTotalGB = 0;
|
||||
let memUsedPercent = 0;
|
||||
if (memResult.success) {
|
||||
memTotalGB = memResult.data.total.toGB();
|
||||
memUsedGB = memResult.data.used.toGB();
|
||||
memUsedPercent = memResult.data.usagePercentage;
|
||||
}
|
||||
|
||||
// Get network stats from network.overview()
|
||||
let netInputBytes = 0;
|
||||
let netOutputBytes = 0;
|
||||
const networkOverview = await osutils.network.overview();
|
||||
if (networkOverview.success) {
|
||||
netInputBytes = networkOverview.data.totalRxBytes.toBytes();
|
||||
netOutputBytes = networkOverview.data.totalTxBytes.toBytes();
|
||||
}
|
||||
|
||||
// Get Block I/O from disk.stats()
|
||||
let blockReadBytes = 0;
|
||||
let blockWriteBytes = 0;
|
||||
const diskStats = await osutils.disk.stats();
|
||||
if (diskStats.success && diskStats.data.length > 0) {
|
||||
// Filter out virtual devices (loop, ram, sr, etc.) - only include real disk devices
|
||||
const excludePatterns = [/^loop/, /^ram/, /^sr\d+$/, /^fd\d+$/];
|
||||
for (const stat of diskStats.data) {
|
||||
// Skip virtual devices
|
||||
if (
|
||||
stat.device &&
|
||||
excludePatterns.some((pattern) => pattern.test(stat.device))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// readBytes and writeBytes are DataSize objects with .toBytes() method
|
||||
blockReadBytes += stat.readBytes.toBytes();
|
||||
blockWriteBytes += stat.writeBytes.toBytes();
|
||||
}
|
||||
}
|
||||
|
||||
// Format values similar to docker stats
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GiB`;
|
||||
}
|
||||
if (bytes >= 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)}MiB`;
|
||||
}
|
||||
if (bytes >= 1024) {
|
||||
return `${(bytes / 1024).toFixed(2)}KiB`;
|
||||
}
|
||||
return `${bytes}B`;
|
||||
};
|
||||
|
||||
// Format memory usage similar to docker stats format: "used / total"
|
||||
const memUsedFormatted = `${memUsedGB.toFixed(2)}GiB`;
|
||||
const memTotalFormatted = `${memTotalGB.toFixed(2)}GiB`;
|
||||
const memUsageFormatted = `${memUsedFormatted} / ${memTotalFormatted}`;
|
||||
|
||||
// Format network I/O
|
||||
const netInputMb = netInputBytes / (1024 * 1024);
|
||||
const netOutputMb = netOutputBytes / (1024 * 1024);
|
||||
const netIOFormatted = `${netInputMb.toFixed(2)}MB / ${netOutputMb.toFixed(2)}MB`;
|
||||
|
||||
// Format Block I/O
|
||||
const blockIOFormatted = `${formatBytes(blockReadBytes)} / ${formatBytes(blockWriteBytes)}`;
|
||||
|
||||
// Create a stat object compatible with recordAdvancedStats
|
||||
return {
|
||||
CPUPerc: `${cpuUsage.toFixed(2)}%`,
|
||||
MemPerc: `${memUsedPercent.toFixed(2)}%`,
|
||||
MemUsage: memUsageFormatted,
|
||||
BlockIO: blockIOFormatted,
|
||||
NetIO: netIOFormatted,
|
||||
Container: "dokploy",
|
||||
ID: "host-system",
|
||||
Name: "dokploy",
|
||||
};
|
||||
};
|
||||
|
||||
export const getAdvancedStats = async (appName: string) => {
|
||||
return {
|
||||
cpu: await readStatsFile(appName, "cpu"),
|
||||
|
||||
@@ -3,26 +3,27 @@ import {
|
||||
invitation,
|
||||
member,
|
||||
organization,
|
||||
users_temp,
|
||||
user,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { getWebServerSettings } from "./web-server-settings";
|
||||
|
||||
export const findUserById = async (userId: string) => {
|
||||
const user = await db.query.users_temp.findFirst({
|
||||
where: eq(users_temp.id, userId),
|
||||
const userResult = await db.query.user.findFirst({
|
||||
where: eq(user.id, userId),
|
||||
// with: {
|
||||
// account: true,
|
||||
// },
|
||||
});
|
||||
if (!user) {
|
||||
if (!userResult) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
return user;
|
||||
return userResult;
|
||||
};
|
||||
|
||||
export const findOrganizationById = async (organizationId: string) => {
|
||||
@@ -46,7 +47,7 @@ export const isAdminPresent = async () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const findAdmin = async () => {
|
||||
export const findOwner = async () => {
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(member.role, "owner"),
|
||||
with: {
|
||||
@@ -64,7 +65,7 @@ export const findAdmin = async () => {
|
||||
};
|
||||
|
||||
export const getUserByToken = async (token: string) => {
|
||||
const user = await db.query.invitation.findFirst({
|
||||
const userResult = await db.query.invitation.findFirst({
|
||||
where: eq(invitation.id, token),
|
||||
columns: {
|
||||
id: true,
|
||||
@@ -76,29 +77,29 @@ export const getUserByToken = async (token: string) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (!userResult) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Invitation not found",
|
||||
});
|
||||
}
|
||||
|
||||
const userAlreadyExists = await db.query.users_temp.findFirst({
|
||||
where: eq(users_temp.email, user?.email || ""),
|
||||
const userAlreadyExists = await db.query.user.findFirst({
|
||||
where: eq(user.email, userResult?.email || ""),
|
||||
});
|
||||
|
||||
const { expiresAt, ...rest } = user;
|
||||
const { expiresAt, ...rest } = userResult;
|
||||
return {
|
||||
...rest,
|
||||
isExpired: user.expiresAt < new Date(),
|
||||
isExpired: userResult.expiresAt < new Date(),
|
||||
userAlreadyExists: !!userAlreadyExists,
|
||||
};
|
||||
};
|
||||
|
||||
export const removeUserById = async (userId: string) => {
|
||||
await db
|
||||
.delete(users_temp)
|
||||
.where(eq(users_temp.id, userId))
|
||||
.delete(user)
|
||||
.where(eq(user.id, userId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
};
|
||||
@@ -107,10 +108,59 @@ export const getDokployUrl = async () => {
|
||||
if (IS_CLOUD) {
|
||||
return "https://app.dokploy.com";
|
||||
}
|
||||
const admin = await findAdmin();
|
||||
const settings = await getWebServerSettings();
|
||||
|
||||
if (admin.user.host) {
|
||||
return `https://${admin.user.host}`;
|
||||
if (settings?.host) {
|
||||
const protocol = settings?.https ? "https" : "http";
|
||||
return `${protocol}://${settings?.host}`;
|
||||
}
|
||||
return `http://${settings?.serverIp}:${process.env.PORT}`;
|
||||
};
|
||||
|
||||
const TRUSTED_ORIGINS_CACHE_TTL_MS = 30 * 60_000;
|
||||
let trustedOriginsCache: { data: string[]; expiresAt: number } | null = null;
|
||||
|
||||
export const getTrustedOrigins = async () => {
|
||||
const runQuery = async () => {
|
||||
const rows = await db
|
||||
.select({ trustedOrigins: user.trustedOrigins })
|
||||
.from(member)
|
||||
.innerJoin(user, eq(member.userId, user.id))
|
||||
.where(eq(member.role, "owner"));
|
||||
return Array.from(new Set(rows.flatMap((r) => r.trustedOrigins ?? [])));
|
||||
};
|
||||
|
||||
if (IS_CLOUD) {
|
||||
const now = Date.now();
|
||||
if (trustedOriginsCache && now < trustedOriginsCache.expiresAt) {
|
||||
return trustedOriginsCache.data;
|
||||
}
|
||||
try {
|
||||
const trustedOrigins = await runQuery();
|
||||
trustedOriginsCache = {
|
||||
data: trustedOrigins,
|
||||
expiresAt: now + TRUSTED_ORIGINS_CACHE_TTL_MS,
|
||||
};
|
||||
return trustedOrigins;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch trusted origins:", error);
|
||||
return trustedOriginsCache?.data ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await runQuery();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch trusted origins:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getTrustedProviders = async () => {
|
||||
try {
|
||||
const providers = await db.query.ssoProvider.findMany();
|
||||
return providers.map((provider) => provider.providerId);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
return `http://${admin.user.serverIp}:${process.env.PORT}`;
|
||||
};
|
||||
|
||||
@@ -2,12 +2,30 @@ import { db } from "@dokploy/server/db";
|
||||
import { ai } from "@dokploy/server/db/schema";
|
||||
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { generateObject } from "ai";
|
||||
import { generateText, Output } from "ai";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findOrganizationById } from "./admin";
|
||||
import { findServerById } from "./server";
|
||||
import { getWebServerSettings } from "./web-server-settings";
|
||||
|
||||
interface SuggestionItem {
|
||||
id: string;
|
||||
name: string;
|
||||
shortDescription: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface SuggestionsOutput {
|
||||
suggestions: SuggestionItem[];
|
||||
}
|
||||
|
||||
interface DockerOutput {
|
||||
dockerCompose: string;
|
||||
envVariables: Array<{ name: string; value: string }>;
|
||||
domains: Array<{ host: string; port: number; serviceName: string }>;
|
||||
configFiles?: Array<{ content: string; filePath: string }>;
|
||||
}
|
||||
|
||||
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
|
||||
const aiSettings = await db.query.ai.findMany({
|
||||
@@ -60,7 +78,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const suggestVariants = async ({
|
||||
organizationId,
|
||||
organizationId: _organizationId,
|
||||
aiId,
|
||||
input,
|
||||
serverId,
|
||||
@@ -79,8 +97,8 @@ export const suggestVariants = async ({
|
||||
|
||||
let ip = "";
|
||||
if (!IS_CLOUD) {
|
||||
const organization = await findOrganizationById(organizationId);
|
||||
ip = organization?.owner.serverIp || "";
|
||||
const settings = await getWebServerSettings();
|
||||
ip = settings?.serverIp || "";
|
||||
}
|
||||
|
||||
if (serverId) {
|
||||
@@ -90,135 +108,177 @@ export const suggestVariants = async ({
|
||||
ip = "127.0.0.1";
|
||||
}
|
||||
|
||||
const { object } = await generateObject({
|
||||
model,
|
||||
output: "object",
|
||||
schema: z.object({
|
||||
suggestions: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
shortDescription: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
prompt: `
|
||||
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items).
|
||||
|
||||
Return your response as a JSON object with the following structure:
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"id": "project-slug",
|
||||
"name": "Project Name",
|
||||
"shortDescription": "Brief one-line description",
|
||||
"description": "Detailed description"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Important rules for the response:
|
||||
1. Use slug format for the id field (lowercase, hyphenated)
|
||||
2. The description field should ONLY contain a plain text description of the project, its features, and use cases
|
||||
3. Do NOT include any code snippets, configuration examples, or installation instructions in the description
|
||||
4. The shortDescription should be a single-line summary focusing on the main technologies
|
||||
5. All projects should be installable in docker and have docker compose support
|
||||
|
||||
User wants to create a new project with the following details:
|
||||
|
||||
${input}
|
||||
`,
|
||||
const suggestionsSchema = z.object({
|
||||
suggestions: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
shortDescription: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
const suggestionsResult = await generateText({
|
||||
model,
|
||||
// @ts-ignore - Zod + AI SDK Output.object() causes excessively deep instantiation
|
||||
output: Output.object({ schema: suggestionsSchema }),
|
||||
prompt: `
|
||||
Act as advanced DevOps engineer and analyze the user's request to determine the appropriate suggestions (up to 3 items).
|
||||
|
||||
CRITICAL - Read the user's request carefully and follow the appropriate strategy:
|
||||
|
||||
Strategy A - If the user specifies a PARTICULAR APPLICATION/SERVICE (e.g., "deploy Chatwoot", "install sendingtk/chatwoot:develop", "setup Bitwarden"):
|
||||
- Generate different deployment VARIANTS of that SAME application
|
||||
- Each variant should be a different configuration (minimal, full stack, with different databases, development vs production, etc.)
|
||||
- Example: For "Chatwoot" → "Chatwoot with PostgreSQL", "Chatwoot Development", "Chatwoot Full Stack"
|
||||
- The name MUST include the specific application name the user mentioned
|
||||
|
||||
Strategy B - If the user describes a GENERAL NEED or USE CASE (e.g., "personal blog", "project management tool", "chat application"):
|
||||
- Suggest different open source projects that fulfill that need
|
||||
- Each suggestion should be a different tool/platform that solves the same problem
|
||||
- Example: For "personal blog" → "WordPress", "Ghost", "Hugo with Nginx"
|
||||
- The name should be the actual project name
|
||||
|
||||
Return your response as a JSON object with the following structure:
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"id": "project-or-variant-slug",
|
||||
"name": "Project Name or Variant Name",
|
||||
"shortDescription": "Brief one-line description",
|
||||
"description": "Detailed description"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Important rules for the response:
|
||||
1. Use slug format for the id field (lowercase, hyphenated)
|
||||
2. Determine which strategy to use based on whether the user specified a particular application or described a general need
|
||||
3. For Strategy A (specific app): The name must include the app name and describe the variant configuration
|
||||
4. For Strategy B (general need): The name should be the actual project/tool name that fulfills the need
|
||||
5. The description field should ONLY contain a plain text description of the project or variant, its features, and use cases
|
||||
6. Do NOT include any code snippets, configuration examples, or installation instructions in the description
|
||||
7. The shortDescription should be a single-line summary focusing on key technologies or differentiators
|
||||
8. All suggestions should be installable in docker and have docker compose support
|
||||
9. Provide variety in your suggestions - different complexity levels, tech stacks, or approaches
|
||||
|
||||
User wants to create a new project with the following details:
|
||||
|
||||
${input}
|
||||
`,
|
||||
});
|
||||
const object = suggestionsResult.output as SuggestionsOutput | undefined;
|
||||
|
||||
if (object?.suggestions?.length) {
|
||||
const dockerSchema = z.object({
|
||||
dockerCompose: z.string(),
|
||||
envVariables: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
domains: z.array(
|
||||
z.object({
|
||||
host: z.string(),
|
||||
port: z.number(),
|
||||
serviceName: z.string(),
|
||||
}),
|
||||
),
|
||||
configFiles: z
|
||||
.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
filePath: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
const result = [];
|
||||
for (const suggestion of object.suggestions) {
|
||||
try {
|
||||
const { object: docker } = await generateObject({
|
||||
const dockerResult = await generateText({
|
||||
model,
|
||||
output: "object",
|
||||
schema: z.object({
|
||||
dockerCompose: z.string(),
|
||||
envVariables: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
domains: z.array(
|
||||
z.object({
|
||||
host: z.string(),
|
||||
port: z.number(),
|
||||
serviceName: z.string(),
|
||||
}),
|
||||
),
|
||||
configFiles: z
|
||||
.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
filePath: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
// @ts-ignore - Zod + AI SDK Output.object() causes excessively deep instantiation
|
||||
output: Output.object({ schema: dockerSchema }),
|
||||
prompt: `
|
||||
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project.
|
||||
|
||||
Return your response as a JSON object with this structure:
|
||||
{
|
||||
"dockerCompose": "yaml string here",
|
||||
"envVariables": [{"name": "VAR_NAME", "value": "example_value"}],
|
||||
"domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}],
|
||||
"configFiles": [{"content": "file content", "filePath": "path/to/file"}]
|
||||
}
|
||||
|
||||
Note: configFiles is optional - only include it if configuration files are absolutely required.
|
||||
|
||||
Follow these rules:
|
||||
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project.
|
||||
|
||||
Docker Compose Rules:
|
||||
1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml
|
||||
2. Use complex values for passwords/secrets variables
|
||||
3. Don't set container_name field in services
|
||||
4. Don't set version field in the docker compose
|
||||
5. Don't set ports like 'ports: 3000:3000', use 'ports: "3000"' instead
|
||||
6. If a service depends on a database or other service, INCLUDE that service in the docker-compose
|
||||
7. Make sure all required services are defined in the docker-compose
|
||||
Return your response as a JSON object with this structure:
|
||||
{
|
||||
"dockerCompose": "yaml string here",
|
||||
"envVariables": [{"name": "VAR_NAME", "value": "example_value"}],
|
||||
"domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}],
|
||||
"configFiles": [{"content": "file content", "filePath": "path/to/file"}]
|
||||
}
|
||||
|
||||
Volume Mounting and Configuration Rules:
|
||||
1. DO NOT create configuration files unless the service CANNOT work without them
|
||||
2. Most services can work with just environment variables - USE THEM FIRST
|
||||
3. Ask yourself: "Can this be configured with an environment variable instead?"
|
||||
4. If and ONLY IF a config file is absolutely required:
|
||||
- Keep it minimal with only critical settings
|
||||
- Use "../files/" prefix for all mounts
|
||||
- Format: "../files/folder:/container/path"
|
||||
5. DO NOT add configuration files for:
|
||||
- Default configurations that work out of the box
|
||||
- Settings that can be handled by environment variables
|
||||
- Proxy or routing configurations (these are handled elsewhere)
|
||||
Note: configFiles is optional - only include it if configuration files are absolutely required.
|
||||
|
||||
Environment Variables Rules:
|
||||
1. For the envVariables array, provide ACTUAL example values, not placeholders
|
||||
2. Use realistic example values (e.g., "admin@example.com" for emails, "mypassword123" for passwords)
|
||||
3. DO NOT use \${VARIABLE_NAME-default} syntax in the envVariables values
|
||||
4. ONLY include environment variables that are actually used in the docker-compose
|
||||
5. Every environment variable referenced in the docker-compose MUST have a corresponding entry in envVariables
|
||||
6. Do not include environment variables for services that don't exist in the docker-compose
|
||||
|
||||
For each service that needs to be exposed to the internet:
|
||||
1. Define a domain configuration with:
|
||||
- host: the domain name for the service in format: {service-name}-{random-3-chars-hex}-${ip ? ip.replaceAll(".", "-") : ""}.traefik.me
|
||||
- port: the internal port the service runs on
|
||||
- serviceName: the name of the service in the docker-compose
|
||||
2. Make sure the service is properly configured to work with the specified port
|
||||
|
||||
Project details:
|
||||
${suggestion?.description}
|
||||
`,
|
||||
Follow these rules:
|
||||
|
||||
Docker Compose Rules:
|
||||
1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml
|
||||
2. Use complex values for passwords/secrets variables
|
||||
3. Don't set container_name field in services
|
||||
4. Don't set version field in the docker compose
|
||||
5. Don't set ports like 'ports: 3000:3000', use 'ports: "3000"' instead
|
||||
6. If a service depends on a database or other service, INCLUDE that service in the docker-compose
|
||||
7. Make sure all required services are defined in the docker-compose
|
||||
|
||||
Docker Image Rules (CRITICAL):
|
||||
1. ALWAYS use 'image:' field, NEVER use 'build:' field
|
||||
2. NEVER use 'build: .' or any build directive - we don't have local Dockerfiles
|
||||
3. Use images from Docker Hub or other public registries (e.g., docker.io, ghcr.io, quay.io)
|
||||
4. For dependencies (databases, redis, etc.), use official images (e.g., postgres:16, redis:7, etc.)
|
||||
5. Always specify image tags - avoid using 'latest' tag, use specific versions when possible
|
||||
6. Examples of correct image usage:
|
||||
- image: sendingtk/chatwoot:develop
|
||||
- image: postgres:16-alpine
|
||||
- image: redis:7-alpine
|
||||
- image: chatwoot/chatwoot:latest
|
||||
7. Examples of INCORRECT usage (DO NOT USE):
|
||||
- build: .
|
||||
- build: ./app
|
||||
- build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
||||
Volume Mounting and Configuration Rules:
|
||||
1. DO NOT create configuration files unless the service CANNOT work without them
|
||||
2. Most services can work with just environment variables - USE THEM FIRST
|
||||
3. Ask yourself: "Can this be configured with an environment variable instead?"
|
||||
4. If and ONLY IF a config file is absolutely required:
|
||||
- Keep it minimal with only critical settings
|
||||
- Use "../files/" prefix for all mounts
|
||||
- Format: "../files/folder:/container/path"
|
||||
5. DO NOT add configuration files for:
|
||||
- Default configurations that work out of the box
|
||||
- Settings that can be handled by environment variables
|
||||
- Proxy or routing configurations (these are handled elsewhere)
|
||||
|
||||
Environment Variables Rules:
|
||||
1. For the envVariables array, provide ACTUAL example values, not placeholders
|
||||
2. Use realistic example values (e.g., "admin@example.com" for emails, "mypassword123" for passwords)
|
||||
3. DO NOT use \${VARIABLE_NAME-default} syntax in the envVariables values
|
||||
4. ONLY include environment variables that are actually used in the docker-compose
|
||||
5. Every environment variable referenced in the docker-compose MUST have a corresponding entry in envVariables
|
||||
6. Do not include environment variables for services that don't exist in the docker-compose
|
||||
|
||||
For each service that needs to be exposed to the internet:
|
||||
1. Define a domain configuration with:
|
||||
- host: the domain name for the service in format: {service-name}-{random-3-chars-hex}-${ip ? ip.replaceAll(".", "-") : ""}.traefik.me
|
||||
- port: the internal port the service runs on
|
||||
- serviceName: the name of the service in the docker-compose
|
||||
2. Make sure the service is properly configured to work with the specified port
|
||||
|
||||
User's original request: ${input}
|
||||
|
||||
Project details:
|
||||
${suggestion?.description}
|
||||
`,
|
||||
});
|
||||
if (!!docker && !!docker.dockerCompose) {
|
||||
const docker = dockerResult.output as DockerOutput | undefined;
|
||||
if (docker?.dockerCompose) {
|
||||
result.push({
|
||||
...suggestion,
|
||||
...docker,
|
||||
|
||||
@@ -7,45 +7,35 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
|
||||
import {
|
||||
buildApplication,
|
||||
getBuildCommand,
|
||||
mechanizeDockerContainer,
|
||||
} from "@dokploy/server/utils/builders";
|
||||
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
||||
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
cloneBitbucketRepository,
|
||||
getBitbucketCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/bitbucket";
|
||||
import {
|
||||
buildDocker,
|
||||
buildRemoteDocker,
|
||||
} from "@dokploy/server/utils/providers/docker";
|
||||
ExecError,
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
|
||||
import { buildRemoteDocker } from "@dokploy/server/utils/providers/docker";
|
||||
import {
|
||||
cloneGitRepository,
|
||||
getCustomGitCloneCommand,
|
||||
getGitCommitInfo,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/github";
|
||||
import {
|
||||
cloneGitlabRepository,
|
||||
getGitlabCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitlab";
|
||||
import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
|
||||
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
|
||||
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
||||
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import {
|
||||
createDeployment,
|
||||
createDeploymentPreview,
|
||||
updateDeployment,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
import { type Domain, getDomainHost } from "./domain";
|
||||
@@ -55,16 +45,16 @@ import {
|
||||
issueCommentExists,
|
||||
updateIssueComment,
|
||||
} from "./github";
|
||||
import { generateApplyPatchesCommand } from "./patch";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { createRollback } from "./rollbacks";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
input: typeof apiCreateApplication._type,
|
||||
input: z.infer<typeof apiCreateApplication>,
|
||||
) => {
|
||||
const appName = buildAppName("app", input.appName);
|
||||
|
||||
@@ -123,6 +113,8 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
gitea: true,
|
||||
server: true,
|
||||
previewDeployments: true,
|
||||
buildRegistry: true,
|
||||
rollbackRegistry: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
@@ -183,6 +175,11 @@ export const deployApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
const applicationEntity = {
|
||||
...application,
|
||||
serverId: serverId,
|
||||
};
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
const deployment = await createDeployment({
|
||||
@@ -192,44 +189,42 @@ export const deployApplication = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...application,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += await cloneGithubRepository(applicationEntity);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += await cloneGitlabRepository(applicationEntity);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += await cloneGiteaRepository(applicationEntity);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
command += await cloneBitbucketRepository(applicationEntity);
|
||||
} else if (application.sourceType === "git") {
|
||||
await cloneGitRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "drop") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += await cloneGitRepository(applicationEntity);
|
||||
} else if (application.sourceType === "docker") {
|
||||
command += await buildRemoteDocker(application);
|
||||
}
|
||||
|
||||
if (application.sourceType !== "docker") {
|
||||
command += await generateApplyPatchesCommand({
|
||||
id: application.applicationId,
|
||||
type: "application",
|
||||
serverId,
|
||||
});
|
||||
}
|
||||
|
||||
command += await getBuildCommand(application);
|
||||
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.environment.project.name,
|
||||
applicationName: application.name,
|
||||
@@ -237,8 +232,24 @@ export const deployApplication = async ({
|
||||
buildLink,
|
||||
organizationId: application.environment.project.organizationId,
|
||||
domains: application.domains,
|
||||
environmentName: application.environment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
|
||||
@@ -253,8 +264,22 @@ export const deployApplication = async ({
|
||||
});
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
// Only extract commit info for non-docker sources
|
||||
if (application.sourceType !== "docker") {
|
||||
const commitInfo = await getGitCommitInfo({
|
||||
appName: application.appName,
|
||||
type: "application",
|
||||
serverId: serverId,
|
||||
});
|
||||
if (commitInfo) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
title: commitInfo.message,
|
||||
description: `Commit: ${commitInfo.hash}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -268,50 +293,9 @@ export const rebuildApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
if (application.sourceType === "github") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "docker") {
|
||||
await buildDocker(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "drop") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployRemoteApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Manual deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
@@ -319,53 +303,19 @@ export const deployRemoteApplication = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
command += await getGitlabCloneCommand(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
command += await getBitbucketCloneCommand(
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
command += await getCustomGitCloneCommand(
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "docker") {
|
||||
command += await buildRemoteDocker(application, deployment.logPath);
|
||||
}
|
||||
|
||||
if (application.sourceType !== "docker") {
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
}
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
await mechanizeDockerContainer(application);
|
||||
let command = "set -e;";
|
||||
// Check case for docker only
|
||||
command += await getBuildCommand(application);
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await mechanizeDockerContainer(application);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.environment.project.name,
|
||||
applicationName: application.name,
|
||||
@@ -373,32 +323,26 @@ export const deployRemoteApplication = async ({
|
||||
buildLink,
|
||||
organizationId: application.environment.project.organizationId,
|
||||
domains: application.domains,
|
||||
environmentName: application.environment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
let command = "";
|
||||
|
||||
const encodedContent = encodeBase64(errorMessage);
|
||||
|
||||
await execAsyncRemote(
|
||||
application.serverId,
|
||||
`
|
||||
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
||||
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
||||
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
||||
);
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
|
||||
await sendBuildErrorNotifications({
|
||||
projectName: application.environment.project.name,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
errorMessage: `Please check the logs for details: ${errorMessage}`,
|
||||
buildLink,
|
||||
organizationId: application.environment.project.organizationId,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -472,16 +416,29 @@ export const deployPreviewApplication = async ({
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.rollbackActive = false;
|
||||
application.buildRegistry = null;
|
||||
application.rollbackRegistry = null;
|
||||
application.registry = null;
|
||||
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
command += await cloneGithubRepository({
|
||||
...application,
|
||||
appName: previewDeployment.appName,
|
||||
branch: previewDeployment.branch,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
command += await getBuildCommand(application);
|
||||
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
}
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
@@ -512,9 +469,9 @@ export const deployPreviewApplication = async ({
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployRemotePreviewApplication = async ({
|
||||
export const rebuildPreviewApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Preview Deployment",
|
||||
titleLog = "Rebuild Preview Deployment",
|
||||
descriptionLog = "",
|
||||
previewDeploymentId,
|
||||
}: {
|
||||
@@ -524,6 +481,8 @@ export const deployRemotePreviewApplication = async ({
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
@@ -531,13 +490,6 @@ export const deployRemotePreviewApplication = async ({
|
||||
previewDeploymentId: previewDeploymentId,
|
||||
});
|
||||
|
||||
const previewDeployment =
|
||||
await findPreviewDeploymentById(previewDeploymentId);
|
||||
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
|
||||
const issueParams = {
|
||||
owner: application?.owner || "",
|
||||
@@ -546,6 +498,7 @@ export const deployRemotePreviewApplication = async ({
|
||||
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
|
||||
githubId: application?.githubId || "",
|
||||
};
|
||||
|
||||
try {
|
||||
const commentExists = await issueCommentExists({
|
||||
...issueParams,
|
||||
@@ -568,6 +521,7 @@ export const deployRemotePreviewApplication = async ({
|
||||
|
||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||
}
|
||||
|
||||
const buildingComment = getIssueComment(
|
||||
application.name,
|
||||
"running",
|
||||
@@ -577,26 +531,28 @@ export const deployRemotePreviewApplication = async ({
|
||||
...issueParams,
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
|
||||
// Set application properties for preview deployment
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.rollbackActive = false;
|
||||
application.buildRegistry = null;
|
||||
application.rollbackRegistry = null;
|
||||
application.registry = null;
|
||||
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
appName: previewDeployment.appName,
|
||||
branch: previewDeployment.branch,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
}
|
||||
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
await mechanizeDockerContainer(application);
|
||||
const serverId = application.serverId;
|
||||
let command = "set -e;";
|
||||
// Only rebuild, don't clone repository
|
||||
command += await getBuildCommand(application);
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
@@ -612,6 +568,23 @@ export const deployRemotePreviewApplication = async ({
|
||||
previewStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||
await updateIssueComment({
|
||||
...issueParams,
|
||||
@@ -627,55 +600,10 @@ export const deployRemotePreviewApplication = async ({
|
||||
return true;
|
||||
};
|
||||
|
||||
export const rebuildRemoteApplication = async ({
|
||||
applicationId,
|
||||
titleLog = "Rebuild deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
applicationId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
if (application.serverId) {
|
||||
if (application.sourceType !== "docker") {
|
||||
let command = "set -e;";
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const encodedContent = encodeBase64(error?.message);
|
||||
|
||||
await execAsyncRemote(
|
||||
application.serverId,
|
||||
`
|
||||
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
||||
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
||||
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
||||
);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getApplicationStats = async (appName: string) => {
|
||||
if (appName === "dokploy") {
|
||||
return await getAdvancedStats(appName);
|
||||
}
|
||||
const filter = {
|
||||
status: ["running"],
|
||||
label: [`com.docker.swarm.service.name=${appName}`],
|
||||
|
||||
@@ -2,17 +2,16 @@ import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateBackup, backups } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Backup = typeof backups.$inferSelect;
|
||||
|
||||
export type BackupSchedule = Awaited<ReturnType<typeof findBackupById>>;
|
||||
export type BackupScheduleList = Awaited<ReturnType<typeof findBackupsByDbId>>;
|
||||
export const createBackup = async (input: typeof apiCreateBackup._type) => {
|
||||
export const createBackup = async (input: z.infer<typeof apiCreateBackup>) => {
|
||||
const newBackup = await db
|
||||
.insert(backups)
|
||||
.values({
|
||||
...input,
|
||||
})
|
||||
.values({ ...input } as typeof backups.$inferInsert)
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
@@ -34,6 +33,7 @@ export const findBackupById = async (backupId: string) => {
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
destination: true,
|
||||
compose: true,
|
||||
},
|
||||
@@ -73,7 +73,7 @@ export const removeBackupById = async (backupId: string) => {
|
||||
|
||||
export const findBackupsByDbId = async (
|
||||
id: string,
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo",
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo" | "libsql",
|
||||
) => {
|
||||
const result = await db.query.backups.findMany({
|
||||
where: eq(backups[`${type}Id`], id),
|
||||
@@ -82,6 +82,7 @@ export const findBackupsByDbId = async (
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,11 +7,12 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Bitbucket = typeof bitbucket.$inferSelect;
|
||||
|
||||
export const createBitbucket = async (
|
||||
input: typeof apiCreateBitbucket._type,
|
||||
input: z.infer<typeof apiCreateBitbucket>,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
@@ -65,7 +66,7 @@ export const findBitbucketById = async (bitbucketId: string) => {
|
||||
|
||||
export const updateBitbucket = async (
|
||||
bitbucketId: string,
|
||||
input: typeof apiUpdateBitbucket._type,
|
||||
input: z.infer<typeof apiUpdateBitbucket>,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
// First get the current bitbucket provider to get gitProviderId
|
||||
|
||||
@@ -78,7 +78,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"89.187.188.227",
|
||||
"89.187.188.228",
|
||||
"139.180.134.196",
|
||||
"89.38.96.158",
|
||||
"89.187.162.249",
|
||||
"89.187.162.242",
|
||||
"185.102.217.65",
|
||||
@@ -106,12 +105,9 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"200.25.38.69",
|
||||
"200.25.42.70",
|
||||
"200.25.36.166",
|
||||
"195.206.229.106",
|
||||
"194.242.11.186",
|
||||
"185.164.35.8",
|
||||
"94.20.154.22",
|
||||
"185.93.1.244",
|
||||
"156.59.145.154",
|
||||
"143.244.49.177",
|
||||
"138.199.46.66",
|
||||
"138.199.37.227",
|
||||
@@ -136,7 +132,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"84.17.59.115",
|
||||
"89.187.165.194",
|
||||
"138.199.15.193",
|
||||
"89.35.237.170",
|
||||
"37.19.216.130",
|
||||
"185.93.1.247",
|
||||
"185.93.3.244",
|
||||
@@ -150,6 +145,7 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"84.17.63.178",
|
||||
"200.25.32.131",
|
||||
"37.19.207.34",
|
||||
"37.19.207.38",
|
||||
"192.189.65.146",
|
||||
"143.244.45.177",
|
||||
"185.93.1.249",
|
||||
@@ -168,9 +164,7 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"129.227.217.178",
|
||||
"129.227.217.179",
|
||||
"200.25.69.94",
|
||||
"128.1.52.179",
|
||||
"200.25.16.103",
|
||||
"15.235.54.226",
|
||||
"102.67.138.155",
|
||||
"156.146.43.65",
|
||||
"195.181.163.203",
|
||||
@@ -278,13 +272,11 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"107.155.47.146",
|
||||
"193.201.190.174",
|
||||
"156.59.95.218",
|
||||
"213.170.143.139",
|
||||
"129.227.186.154",
|
||||
"195.238.127.98",
|
||||
"200.25.22.6",
|
||||
"204.16.244.92",
|
||||
"200.25.70.101",
|
||||
"200.25.66.100",
|
||||
"139.180.209.182",
|
||||
"103.108.231.41",
|
||||
"103.108.229.5",
|
||||
@@ -387,46 +379,13 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"38.54.5.37",
|
||||
"38.54.3.92",
|
||||
"185.165.170.74",
|
||||
"207.121.80.118",
|
||||
"207.121.46.228",
|
||||
"207.121.46.236",
|
||||
"207.121.46.244",
|
||||
"207.121.46.252",
|
||||
"216.202.235.164",
|
||||
"207.121.46.220",
|
||||
"207.121.75.132",
|
||||
"207.121.80.12",
|
||||
"207.121.80.172",
|
||||
"207.121.90.60",
|
||||
"207.121.90.68",
|
||||
"207.121.97.204",
|
||||
"207.121.90.252",
|
||||
"207.121.97.236",
|
||||
"207.121.99.12",
|
||||
"138.199.24.219",
|
||||
"185.93.2.251",
|
||||
"138.199.46.65",
|
||||
"207.121.41.196",
|
||||
"207.121.99.20",
|
||||
"207.121.99.36",
|
||||
"207.121.99.44",
|
||||
"207.121.99.52",
|
||||
"207.121.99.60",
|
||||
"207.121.23.68",
|
||||
"207.121.23.124",
|
||||
"207.121.23.244",
|
||||
"207.121.23.180",
|
||||
"207.121.23.188",
|
||||
"207.121.23.196",
|
||||
"207.121.23.204",
|
||||
"207.121.24.52",
|
||||
"207.121.24.60",
|
||||
"207.121.24.68",
|
||||
"207.121.24.76",
|
||||
"207.121.24.92",
|
||||
"207.121.24.100",
|
||||
"207.121.24.108",
|
||||
"207.121.24.116",
|
||||
"154.95.86.76",
|
||||
"5.9.99.73",
|
||||
"78.46.92.118",
|
||||
@@ -434,14 +393,52 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"78.46.156.89",
|
||||
"88.198.9.155",
|
||||
"144.76.79.22",
|
||||
"103.1.215.93",
|
||||
"103.137.12.33",
|
||||
"103.107.196.31",
|
||||
"116.90.72.155",
|
||||
"103.137.14.5",
|
||||
"116.90.75.65",
|
||||
"37.19.207.37",
|
||||
"208.83.234.224",
|
||||
"79.127.237.104",
|
||||
"79.127.243.187",
|
||||
"45.156.248.73",
|
||||
"79.127.134.225",
|
||||
"79.127.134.226",
|
||||
"79.127.134.227",
|
||||
"79.127.134.228",
|
||||
"79.127.134.229",
|
||||
"79.127.134.230",
|
||||
"79.127.134.231",
|
||||
"79.127.134.130",
|
||||
"79.127.134.131",
|
||||
"79.127.134.132",
|
||||
"79.127.134.234",
|
||||
"79.127.134.235",
|
||||
"185.111.111.154",
|
||||
"185.111.111.155",
|
||||
"185.111.111.156",
|
||||
"185.111.111.157",
|
||||
"185.111.111.158",
|
||||
"185.111.111.159",
|
||||
"185.111.111.160",
|
||||
"141.227.142.242",
|
||||
"94.128.254.166",
|
||||
"195.206.229.69",
|
||||
"200.25.86.90",
|
||||
"148.113.190.161",
|
||||
"46.151.194.242",
|
||||
"46.151.194.243",
|
||||
"212.102.40.120",
|
||||
"213.170.143.100",
|
||||
"154.93.86.71",
|
||||
"143.244.60.196",
|
||||
"143.244.60.197",
|
||||
"143.244.60.195",
|
||||
"79.127.134.129",
|
||||
"79.127.134.133",
|
||||
"152.233.22.97",
|
||||
"152.233.22.98",
|
||||
"152.233.22.100",
|
||||
"152.233.22.99",
|
||||
"152.233.22.101",
|
||||
"152.233.22.102",
|
||||
"152.233.22.103",
|
||||
"116.202.155.146",
|
||||
"116.202.193.178",
|
||||
"116.202.224.168",
|
||||
@@ -502,6 +499,12 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"103.60.15.166",
|
||||
"103.60.15.167",
|
||||
"103.60.15.168",
|
||||
"176.9.139.94",
|
||||
"148.251.129.132",
|
||||
"148.251.131.73",
|
||||
"148.251.131.74",
|
||||
"136.243.70.170",
|
||||
"148.251.131.238",
|
||||
"109.248.43.116",
|
||||
"109.248.43.117",
|
||||
"109.248.43.162",
|
||||
@@ -527,7 +530,9 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"139.180.129.216",
|
||||
"139.99.174.7",
|
||||
"89.187.169.18",
|
||||
"143.244.38.133",
|
||||
"89.187.179.7",
|
||||
"169.150.213.50",
|
||||
"143.244.62.213",
|
||||
"185.93.3.246",
|
||||
"195.181.163.198",
|
||||
@@ -535,7 +540,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"84.17.37.211",
|
||||
"212.102.50.54",
|
||||
"212.102.46.115",
|
||||
"143.244.38.135",
|
||||
"169.150.238.21",
|
||||
"169.150.207.51",
|
||||
"169.150.207.49",
|
||||
@@ -546,7 +550,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"169.150.247.139",
|
||||
"169.150.247.177",
|
||||
"169.150.247.178",
|
||||
"169.150.213.49",
|
||||
"212.102.46.119",
|
||||
"84.17.38.234",
|
||||
"84.17.38.233",
|
||||
@@ -558,7 +561,6 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"169.150.247.138",
|
||||
"169.150.247.184",
|
||||
"169.150.247.185",
|
||||
"156.146.58.83",
|
||||
"212.102.43.88",
|
||||
"89.187.169.26",
|
||||
"109.61.89.57",
|
||||
@@ -587,6 +589,17 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"138.199.4.177",
|
||||
"37.19.222.34",
|
||||
"46.151.193.85",
|
||||
"79.127.237.99",
|
||||
"212.104.158.30",
|
||||
"212.104.158.31",
|
||||
"212.104.158.32",
|
||||
"212.104.158.33",
|
||||
"212.104.158.34",
|
||||
"212.104.158.28",
|
||||
"212.104.158.29",
|
||||
"212.104.158.35",
|
||||
"212.104.158.36",
|
||||
"212.104.158.37",
|
||||
"212.104.158.17",
|
||||
"212.104.158.18",
|
||||
"212.104.158.19",
|
||||
@@ -595,14 +608,38 @@ const BUNNY_CDN_IPS = new Set([
|
||||
"212.104.158.22",
|
||||
"212.104.158.24",
|
||||
"212.104.158.26",
|
||||
"79.127.237.134",
|
||||
"89.187.184.177",
|
||||
"89.187.184.179",
|
||||
"89.187.184.173",
|
||||
"89.187.184.178",
|
||||
"89.187.184.176",
|
||||
"212.104.158.25",
|
||||
"212.104.158.27",
|
||||
"212.104.158.67",
|
||||
"212.104.158.10",
|
||||
"212.104.158.12",
|
||||
"212.104.158.64",
|
||||
"212.104.158.16",
|
||||
"212.104.158.23",
|
||||
"212.104.158.54",
|
||||
]);
|
||||
|
||||
// Arvancloud IP ranges
|
||||
// https://www.arvancloud.ir/fa/ips.txt
|
||||
const ARVANCLOUD_IP_RANGES = [
|
||||
"185.143.232.0/22",
|
||||
"188.229.116.16/29",
|
||||
"94.101.182.0/27",
|
||||
"2.144.3.128/28",
|
||||
"89.45.48.64/28",
|
||||
"37.32.16.0/27",
|
||||
"37.32.17.0/27",
|
||||
"37.32.18.0/27",
|
||||
"37.32.19.0/27",
|
||||
"185.215.232.0/22",
|
||||
"178.131.120.48/28",
|
||||
];
|
||||
|
||||
const CDN_PROVIDERS: CDNProvider[] = [
|
||||
{
|
||||
name: "cloudflare",
|
||||
@@ -627,6 +664,14 @@ const CDN_PROVIDERS: CDNProvider[] = [
|
||||
warningMessage:
|
||||
"Domain is behind Fastly - actual IP is masked by CDN proxy",
|
||||
},
|
||||
{
|
||||
name: "arvancloud",
|
||||
displayName: "Arvancloud",
|
||||
checkIp: (ip: string) =>
|
||||
ARVANCLOUD_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Arvancloud - actual IP is masked by CDN proxy",
|
||||
},
|
||||
];
|
||||
|
||||
export const detectCDNProvider = (ip: string): CDNProvider | null => {
|
||||
|
||||
@@ -7,14 +7,10 @@ import {
|
||||
cleanAppName,
|
||||
compose,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import {
|
||||
buildCompose,
|
||||
getBuildComposeCommand,
|
||||
} from "@dokploy/server/utils/builders/compose";
|
||||
import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose";
|
||||
import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose";
|
||||
import {
|
||||
cloneCompose,
|
||||
cloneComposeRemote,
|
||||
loadDockerCompose,
|
||||
loadDockerComposeRemote,
|
||||
} from "@dokploy/server/utils/docker/domain";
|
||||
@@ -22,43 +18,37 @@ import type { ComposeSpecification } from "@dokploy/server/utils/docker/types";
|
||||
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
||||
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
||||
import {
|
||||
ExecError,
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
cloneBitbucketRepository,
|
||||
getBitbucketCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/bitbucket";
|
||||
import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket";
|
||||
import {
|
||||
cloneGitRepository,
|
||||
getCustomGitCloneCommand,
|
||||
getGitCommitInfo,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/github";
|
||||
import {
|
||||
cloneGitlabRepository,
|
||||
getGitlabCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitlab";
|
||||
import {
|
||||
createComposeFile,
|
||||
getCreateComposeFileCommand,
|
||||
} from "@dokploy/server/utils/providers/raw";
|
||||
import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea";
|
||||
import { cloneGithubRepository } from "@dokploy/server/utils/providers/github";
|
||||
import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
||||
import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
||||
import {
|
||||
createDeploymentCompose,
|
||||
updateDeployment,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
import { generateApplyPatchesCommand } from "./patch";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Compose = typeof compose.$inferSelect;
|
||||
|
||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
export const createCompose = async (
|
||||
input: z.infer<typeof apiCreateCompose>,
|
||||
) => {
|
||||
const appName = buildAppName("compose", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
@@ -163,10 +153,11 @@ export const loadServices = async (
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
if (type === "fetch") {
|
||||
const command = await cloneCompose(compose);
|
||||
if (compose.serverId) {
|
||||
await cloneComposeRemote(compose);
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await cloneCompose(compose);
|
||||
await execAsync(command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +218,7 @@ export const deployCompose = async ({
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.environment.projectId
|
||||
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
}/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -235,24 +226,48 @@ export const deployCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
const entity = {
|
||||
...compose,
|
||||
type: "compose" as const,
|
||||
};
|
||||
let command = "set -e;";
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...compose,
|
||||
logPath: deployment.logPath,
|
||||
type: "compose",
|
||||
});
|
||||
command += await cloneGithubRepository(entity);
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(compose, deployment.logPath, true);
|
||||
command += await cloneGitlabRepository(entity);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
||||
command += await cloneBitbucketRepository(entity);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRepository(compose, deployment.logPath, true);
|
||||
command += await cloneGitRepository(entity);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(compose, deployment.logPath, true);
|
||||
command += await cloneGiteaRepository(entity);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
command += getCreateComposeFileCommand(entity);
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
|
||||
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
command = "set -e;";
|
||||
if (compose.sourceType !== "raw") {
|
||||
command += await generateApplyPatchesCommand({
|
||||
id: compose.composeId,
|
||||
type: "compose",
|
||||
serverId: compose.serverId,
|
||||
});
|
||||
}
|
||||
|
||||
command += await getBuildComposeCommand(entity);
|
||||
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
@@ -265,8 +280,24 @@ export const deployCompose = async ({
|
||||
buildLink,
|
||||
organizationId: compose.environment.project.organizationId,
|
||||
domains: compose.domains,
|
||||
environmentName: compose.environment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
@@ -281,6 +312,19 @@ export const deployCompose = async ({
|
||||
organizationId: compose.environment.project.organizationId,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
if (compose.sourceType !== "raw") {
|
||||
const commitInfo = await getGitCommitInfo({
|
||||
...compose,
|
||||
type: "compose",
|
||||
});
|
||||
if (commitInfo) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
title: commitInfo.message,
|
||||
description: `Commit: ${commitInfo.hash}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -302,154 +346,23 @@ export const rebuildCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
let command = "set -e;";
|
||||
if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
command += getCreateComposeFileCommand(compose);
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const deployRemoteCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Manual deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.environment.projectId
|
||||
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
try {
|
||||
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (compose.serverId) {
|
||||
let command = "set -e;";
|
||||
|
||||
if (compose.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...compose,
|
||||
logPath: deployment.logPath,
|
||||
type: "compose",
|
||||
serverId: compose.serverId,
|
||||
});
|
||||
} else if (compose.sourceType === "gitlab") {
|
||||
command += await getGitlabCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
} else if (compose.sourceType === "bitbucket") {
|
||||
command += await getBitbucketCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
} else if (compose.sourceType === "git") {
|
||||
command += await getCustomGitCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
console.log(command);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
command += getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "done",
|
||||
});
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: compose.environment.project.name,
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
organizationId: compose.environment.project.organizationId,
|
||||
domains: compose.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const encodedContent = encodeBase64(error?.message);
|
||||
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`
|
||||
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
||||
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
||||
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
||||
);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
await sendBuildErrorNotifications({
|
||||
projectName: compose.environment.project.name,
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error building",
|
||||
buildLink,
|
||||
organizationId: compose.environment.project.organizationId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const rebuildRemoteCompose = async ({
|
||||
composeId,
|
||||
titleLog = "Rebuild deployment",
|
||||
descriptionLog = "",
|
||||
}: {
|
||||
composeId: string;
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
});
|
||||
|
||||
try {
|
||||
if (compose.sourceType === "raw") {
|
||||
const command = getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
command += await getBuildComposeCommand(compose);
|
||||
commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (compose.serverId) {
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
await execAsyncRemote(compose.serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
@@ -457,16 +370,21 @@ export const rebuildRemoteCompose = async ({
|
||||
composeStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const encodedContent = encodeBase64(error?.message);
|
||||
let command = "";
|
||||
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`
|
||||
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
|
||||
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
||||
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
||||
);
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
@@ -488,20 +406,18 @@ export const removeCompose = async (
|
||||
if (compose.composeType === "stack") {
|
||||
const command = `
|
||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||
cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
||||
docker stack rm ${compose.appName};
|
||||
rm -rf ${projectPath}`;
|
||||
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await execAsync(command, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
} else {
|
||||
const command = `
|
||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||
cd ${projectPath} && docker compose -p ${compose.appName} down ${
|
||||
cd ${projectPath} && env -i PATH="$PATH" docker compose -p ${compose.appName} down ${
|
||||
deleteVolumes ? "--volumes" : ""
|
||||
} && rm -rf ${projectPath}`;
|
||||
|
||||
@@ -528,7 +444,7 @@ export const startCompose = async (composeId: string) => {
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
const path =
|
||||
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
||||
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
|
||||
const baseCommand = `env -i PATH="$PATH" docker compose -p ${compose.appName} -f ${path} up -d`;
|
||||
if (compose.composeType === "docker-compose") {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
@@ -563,14 +479,17 @@ export const stopCompose = async (composeId: string) => {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${
|
||||
`cd ${join(COMPOSE_PATH, compose.appName)} && env -i PATH="$PATH" docker compose -p ${
|
||||
compose.appName
|
||||
} stop`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
||||
cwd: join(COMPOSE_PATH, compose.appName),
|
||||
});
|
||||
await execAsync(
|
||||
`env -i PATH="$PATH" docker compose -p ${compose.appName} stop`,
|
||||
{
|
||||
cwd: join(COMPOSE_PATH, compose.appName),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,21 @@ import {
|
||||
type apiCreateDeploymentSchedule,
|
||||
type apiCreateDeploymentServer,
|
||||
type apiCreateDeploymentVolumeBackup,
|
||||
applications,
|
||||
compose,
|
||||
deployments,
|
||||
environments,
|
||||
projects,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { format } from "date-fns";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { desc, eq, and, inArray, or, sql } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
type Application,
|
||||
findApplicationById,
|
||||
@@ -34,6 +42,41 @@ import { findScheduleById } from "./schedule";
|
||||
import { findServerById, type Server } from "./server";
|
||||
import { findVolumeBackupById } from "./volume-backups";
|
||||
|
||||
export type ServicePath = { href: string | null; label: string };
|
||||
|
||||
export async function resolveServicePath(
|
||||
orgId: string,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<ServicePath> {
|
||||
try {
|
||||
const applicationId = data?.applicationId as string | undefined;
|
||||
const composeId = data?.composeId as string | undefined;
|
||||
if (applicationId) {
|
||||
const app = await findApplicationById(applicationId);
|
||||
if (app.environment.project.organizationId !== orgId) {
|
||||
return { href: null, label: "Application" };
|
||||
}
|
||||
return {
|
||||
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
|
||||
label: "Application",
|
||||
};
|
||||
}
|
||||
if (composeId) {
|
||||
const comp = await findComposeById(composeId);
|
||||
if (comp.environment.project.organizationId !== orgId) {
|
||||
return { href: null, label: "Compose" };
|
||||
}
|
||||
return {
|
||||
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
|
||||
label: "Compose",
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// not found or unauthorized
|
||||
}
|
||||
return { href: null, label: "—" };
|
||||
}
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
|
||||
export const findDeploymentById = async (deploymentId: string) => {
|
||||
@@ -69,29 +112,31 @@ export const findDeploymentByApplicationId = async (applicationId: string) => {
|
||||
|
||||
export const createDeployment = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeployment._type,
|
||||
z.infer<typeof apiCreateDeployment>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
|
||||
await removeLastTenDeployments(
|
||||
deployment.applicationId,
|
||||
"application",
|
||||
application.serverId,
|
||||
);
|
||||
try {
|
||||
await removeLastTenDeployments(
|
||||
deployment.applicationId,
|
||||
"application",
|
||||
application.serverId,
|
||||
);
|
||||
const { LOGS_PATH } = paths(!!application.serverId);
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${application.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, application.appName, fileName);
|
||||
|
||||
if (application.serverId) {
|
||||
const server = await findServerById(application.serverId);
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${application.appName};
|
||||
echo "Initializing deployment" >> ${logFilePath};
|
||||
echo "Building on ${serverId ? "Build Server" : "Dokploy Server"}" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
@@ -99,7 +144,7 @@ export const createDeployment = async (
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
@@ -111,6 +156,9 @@ export const createDeployment = async (
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
startedAt: new Date().toISOString(),
|
||||
...(application.buildServerId && {
|
||||
buildServerId: application.buildServerId,
|
||||
}),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
@@ -145,20 +193,19 @@ export const createDeployment = async (
|
||||
|
||||
export const createDeploymentPreview = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentPreview._type,
|
||||
z.infer<typeof apiCreateDeploymentPreview>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
deployment.previewDeploymentId,
|
||||
);
|
||||
await removeLastTenDeployments(
|
||||
deployment.previewDeploymentId,
|
||||
"previewDeployment",
|
||||
previewDeployment?.application?.serverId,
|
||||
);
|
||||
try {
|
||||
await removeLastTenDeployments(
|
||||
deployment.previewDeploymentId,
|
||||
"previewDeployment",
|
||||
previewDeployment?.application?.serverId,
|
||||
);
|
||||
|
||||
const appName = `${previewDeployment.appName}`;
|
||||
const { LOGS_PATH } = paths(!!previewDeployment?.application?.serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
@@ -228,17 +275,17 @@ export const createDeploymentPreview = async (
|
||||
|
||||
export const createDeploymentCompose = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentCompose._type,
|
||||
z.infer<typeof apiCreateDeploymentCompose>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const compose = await findComposeById(deployment.composeId);
|
||||
await removeLastTenDeployments(
|
||||
deployment.composeId,
|
||||
"compose",
|
||||
compose.serverId,
|
||||
);
|
||||
try {
|
||||
await removeLastTenDeployments(
|
||||
deployment.composeId,
|
||||
"compose",
|
||||
compose.serverId,
|
||||
);
|
||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${compose.appName}-${formattedDateTime}.log`;
|
||||
@@ -249,7 +296,7 @@ export const createDeploymentCompose = async (
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${compose.appName};
|
||||
echo "Initializing deployment" >> ${logFilePath};
|
||||
echo "Initializing deployment\n" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
@@ -257,7 +304,7 @@ echo "Initializing deployment" >> ${logFilePath};
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment");
|
||||
await fsPromises.writeFile(logFilePath, "Initializing deployment\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
@@ -305,7 +352,7 @@ echo "Initializing deployment" >> ${logFilePath};
|
||||
|
||||
export const createDeploymentBackup = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentBackup._type,
|
||||
z.infer<typeof apiCreateDeploymentBackup>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
@@ -321,8 +368,8 @@ export const createDeploymentBackup = async (
|
||||
} else if (backup.backupType === "compose") {
|
||||
serverId = backup.compose?.serverId;
|
||||
}
|
||||
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
|
||||
try {
|
||||
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${backup.appName}-${formattedDateTime}.log`;
|
||||
@@ -385,18 +432,18 @@ echo "Initializing backup\n" >> ${logFilePath};
|
||||
|
||||
export const createDeploymentSchedule = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentSchedule._type,
|
||||
z.infer<typeof apiCreateDeploymentSchedule>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const schedule = await findScheduleById(deployment.scheduleId);
|
||||
|
||||
const serverId =
|
||||
schedule.application?.serverId ||
|
||||
schedule.compose?.serverId ||
|
||||
schedule.server?.serverId;
|
||||
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
|
||||
try {
|
||||
const serverId =
|
||||
schedule.application?.serverId ||
|
||||
schedule.compose?.serverId ||
|
||||
schedule.server?.serverId;
|
||||
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
|
||||
const { SCHEDULES_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
|
||||
@@ -461,20 +508,20 @@ export const createDeploymentSchedule = async (
|
||||
|
||||
export const createDeploymentVolumeBackup = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentVolumeBackup._type,
|
||||
z.infer<typeof apiCreateDeploymentVolumeBackup>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const volumeBackup = await findVolumeBackupById(deployment.volumeBackupId);
|
||||
|
||||
const serverId =
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
await removeLastTenDeployments(
|
||||
deployment.volumeBackupId,
|
||||
"volumeBackup",
|
||||
serverId,
|
||||
);
|
||||
try {
|
||||
const serverId =
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
await removeLastTenDeployments(
|
||||
deployment.volumeBackupId,
|
||||
"volumeBackup",
|
||||
serverId,
|
||||
);
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`;
|
||||
@@ -549,11 +596,27 @@ export const removeDeployment = async (deploymentId: string) => {
|
||||
const deployment = await db
|
||||
.delete(deployments)
|
||||
.where(eq(deployments.deploymentId, deploymentId))
|
||||
.returning();
|
||||
return deployment[0];
|
||||
.returning()
|
||||
.then((result) => result[0]);
|
||||
|
||||
if (!deployment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const logPath = path.join(deployment.logPath);
|
||||
if (logPath && logPath !== ".") {
|
||||
const command = `rm -f ${logPath};`;
|
||||
if (deployment.serverId) {
|
||||
await execAsyncRemote(deployment.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
}
|
||||
|
||||
return deployment;
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Error creating the deployment";
|
||||
error instanceof Error ? error.message : "Error removing the deployment";
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message,
|
||||
@@ -621,34 +684,49 @@ const removeLastTenDeployments = async (
|
||||
if (serverId) {
|
||||
let command = "";
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
try {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
|
||||
if (logPath !== ".") {
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
if (logPath && logPath !== ".") {
|
||||
command += `rm -rf ${logPath};`;
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
if (command) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
}
|
||||
} else {
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
try {
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (
|
||||
logPath &&
|
||||
logPath !== "." &&
|
||||
existsSync(logPath) &&
|
||||
!oldDeployment.errorMessage
|
||||
) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to remove deployment ${oldDeployment.deploymentId} during cleanup:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (
|
||||
existsSync(logPath) &&
|
||||
!oldDeployment.errorMessage &&
|
||||
logPath !== "."
|
||||
) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -712,6 +790,135 @@ export const findAllDeploymentsByComposeId = async (composeId: string) => {
|
||||
return deploymentsList;
|
||||
};
|
||||
|
||||
const centralizedDeploymentsWith = {
|
||||
application: {
|
||||
columns: { applicationId: true, name: true, appName: true },
|
||||
with: {
|
||||
environment: {
|
||||
columns: { environmentId: true, name: true },
|
||||
with: {
|
||||
project: {
|
||||
columns: { projectId: true, name: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
columns: { serverId: true, name: true, serverType: true },
|
||||
},
|
||||
buildServer: {
|
||||
columns: { serverId: true, name: true, serverType: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
columns: { composeId: true, name: true, appName: true },
|
||||
with: {
|
||||
environment: {
|
||||
columns: { environmentId: true, name: true },
|
||||
with: {
|
||||
project: {
|
||||
columns: { projectId: true, name: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
columns: { serverId: true, name: true, serverType: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
columns: { serverId: true, name: true, serverType: true },
|
||||
},
|
||||
buildServer: {
|
||||
columns: { serverId: true, name: true, serverType: true },
|
||||
},
|
||||
} as const;
|
||||
|
||||
async function getApplicationIdsInOrg(
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
): Promise<string[]> {
|
||||
const rows = await db
|
||||
.select({ applicationId: applications.applicationId })
|
||||
.from(applications)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(applications.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(
|
||||
accessedServices !== null
|
||||
? and(
|
||||
eq(projects.organizationId, orgId),
|
||||
inArray(applications.applicationId, accessedServices),
|
||||
)
|
||||
: eq(projects.organizationId, orgId),
|
||||
);
|
||||
return rows.map((r) => r.applicationId);
|
||||
}
|
||||
|
||||
async function getComposeIdsInOrg(
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
): Promise<string[]> {
|
||||
const rows = await db
|
||||
.select({ composeId: compose.composeId })
|
||||
.from(compose)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(compose.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(
|
||||
accessedServices !== null
|
||||
? and(
|
||||
eq(projects.organizationId, orgId),
|
||||
inArray(compose.composeId, accessedServices),
|
||||
)
|
||||
: eq(projects.organizationId, orgId),
|
||||
);
|
||||
return rows.map((r) => r.composeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* All deployments for applications and compose in the org.
|
||||
* Pass accessedServices for members (only those services), null for owner/admin.
|
||||
*/
|
||||
export const findAllDeploymentsCentralized = async (
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
) => {
|
||||
if (accessedServices !== null && accessedServices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [appIds, compIds] = await Promise.all([
|
||||
getApplicationIdsInOrg(orgId, accessedServices),
|
||||
getComposeIdsInOrg(orgId, accessedServices),
|
||||
]);
|
||||
|
||||
if (appIds.length === 0 && compIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const conditions = [
|
||||
...(appIds.length > 0 ? [inArray(deployments.applicationId, appIds)] : []),
|
||||
...(compIds.length > 0 ? [inArray(deployments.composeId, compIds)] : []),
|
||||
];
|
||||
const whereClause =
|
||||
conditions.length === 0
|
||||
? sql`1 = 0`
|
||||
: conditions.length === 1
|
||||
? conditions[0]
|
||||
: or(...conditions);
|
||||
|
||||
return db.query.deployments.findMany({
|
||||
where: whereClause,
|
||||
orderBy: desc(deployments.createdAt),
|
||||
with: centralizedDeploymentsWith,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateDeployment = async (
|
||||
deploymentId: string,
|
||||
deploymentData: Partial<Deployment>,
|
||||
@@ -748,7 +955,7 @@ export const updateDeploymentStatus = async (
|
||||
|
||||
export const createServerDeployment = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentServer._type,
|
||||
z.infer<typeof apiCreateDeploymentServer>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
@@ -826,3 +1033,19 @@ export const findAllDeploymentsByServerId = async (serverId: string) => {
|
||||
});
|
||||
return deploymentsList;
|
||||
};
|
||||
|
||||
export const clearOldDeployments = async (
|
||||
appName: string,
|
||||
serverId: string | null,
|
||||
) => {
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const folder = path.join(LOGS_PATH, appName);
|
||||
const command = `
|
||||
rm -rf ${folder};
|
||||
`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Destination = typeof destinations.$inferSelect;
|
||||
|
||||
export const createDestintation = async (
|
||||
input: typeof apiCreateDestination._type,
|
||||
input: z.infer<typeof apiCreateDestination>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const newDestination = await db
|
||||
|
||||
@@ -109,7 +109,7 @@ export const getContainersByAppNameMatch = async (
|
||||
try {
|
||||
let result: string[] = [];
|
||||
const cmd =
|
||||
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'";
|
||||
"docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}} | Status: {{.Status}}'";
|
||||
|
||||
const command =
|
||||
appType === "docker-compose"
|
||||
@@ -148,10 +148,14 @@ export const getContainersByAppNameMatch = async (
|
||||
const state = parts[2]
|
||||
? parts[2].replace("State: ", "").trim()
|
||||
: "No state";
|
||||
|
||||
const status = parts[3] ? parts[3].replace("Status: ", "").trim() : "";
|
||||
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
status,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -168,7 +172,9 @@ export const getStackContainersByAppName = async (
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker stack ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
const command = `docker stack ps ${appName} --no-trunc --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}} | CurrentState: {{.CurrentState}} | Error: {{.Error}}'`;
|
||||
|
||||
console.log("command ", command);
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
@@ -205,11 +211,17 @@ export const getStackContainersByAppName = async (
|
||||
const node = parts[3]
|
||||
? parts[3].replace("Node: ", "").trim()
|
||||
: "No specific node";
|
||||
const currentState = parts[4]
|
||||
? parts[4].replace("CurrentState: ", "").trim()
|
||||
: "";
|
||||
const error = parts[5] ? parts[5].replace("Error: ", "").trim() : "";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
node,
|
||||
currentState,
|
||||
error,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -226,8 +238,7 @@ export const getServiceContainersByAppName = async (
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker service ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
|
||||
const command = `docker service ps ${appName} --no-trunc --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}} | CurrentState: {{.CurrentState}} | Error: {{.Error}}'`;
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
@@ -265,11 +276,18 @@ export const getServiceContainersByAppName = async (
|
||||
const node = parts[3]
|
||||
? parts[3].replace("Node: ", "").trim()
|
||||
: "No specific node";
|
||||
|
||||
const currentState = parts[4]
|
||||
? parts[4].replace("CurrentState: ", "").trim()
|
||||
: "";
|
||||
const error = parts[5] ? parts[5].replace("Error: ", "").trim() : "";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
currentState,
|
||||
node,
|
||||
error,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||
import { generateRandomDomain } from "@dokploy/server/templates";
|
||||
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { type apiCreateDomain, domains } from "../db/schema";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { detectCDNProvider } from "./cdn";
|
||||
import { findServerById } from "./server";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
|
||||
export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
||||
export const createDomain = async (input: z.infer<typeof apiCreateDomain>) => {
|
||||
const result = await db.transaction(async (tx) => {
|
||||
const domain = await tx
|
||||
.insert(domains)
|
||||
.values({
|
||||
...input,
|
||||
})
|
||||
host: input.host?.trim(),
|
||||
} as typeof domains.$inferInsert)
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
@@ -43,7 +45,7 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
|
||||
|
||||
export const generateTraefikMeDomain = async (
|
||||
appName: string,
|
||||
userId: string,
|
||||
_userId: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
if (serverId) {
|
||||
@@ -60,9 +62,9 @@ export const generateTraefikMeDomain = async (
|
||||
projectName: appName,
|
||||
});
|
||||
}
|
||||
const admin = await findUserById(userId);
|
||||
const settings = await getWebServerSettings();
|
||||
return generateRandomDomain({
|
||||
serverIp: admin?.serverIp || "",
|
||||
serverIp: settings?.serverIp || "",
|
||||
projectName: appName,
|
||||
});
|
||||
};
|
||||
@@ -120,6 +122,7 @@ export const updateDomainById = async (
|
||||
.update(domains)
|
||||
.set({
|
||||
...domainData,
|
||||
...(domainData.host && { host: domainData.host.trim() }),
|
||||
})
|
||||
.where(eq(domains.domainId, domainId))
|
||||
.returning();
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Environment = typeof environments.$inferSelect;
|
||||
|
||||
export const createEnvironment = async (
|
||||
input: typeof apiCreateEnvironment._type,
|
||||
input: z.infer<typeof apiCreateEnvironment>,
|
||||
) => {
|
||||
const newEnvironment = await db
|
||||
.insert(environments)
|
||||
@@ -33,14 +34,159 @@ export const createEnvironment = async (
|
||||
export const findEnvironmentById = async (environmentId: string) => {
|
||||
const environment = await db.query.environments.findFirst({
|
||||
where: eq(environments.environmentId, environmentId),
|
||||
columns: {
|
||||
name: true,
|
||||
description: true,
|
||||
environmentId: true,
|
||||
isDefault: true,
|
||||
projectId: true,
|
||||
env: true,
|
||||
},
|
||||
with: {
|
||||
applications: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
applications: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
name: true,
|
||||
applicationId: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
mongoId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
postgresId: true,
|
||||
name: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
redisId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
composeId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
composeStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
libsqlId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
@@ -65,18 +211,47 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
libsql: true,
|
||||
project: true,
|
||||
},
|
||||
columns: {
|
||||
name: true,
|
||||
description: true,
|
||||
environmentId: true,
|
||||
isDefault: true,
|
||||
},
|
||||
});
|
||||
return projectEnvironments;
|
||||
};
|
||||
|
||||
const environmentHasServices = (
|
||||
env: Awaited<ReturnType<typeof findEnvironmentById>>,
|
||||
) => {
|
||||
return (
|
||||
(env.applications?.length ?? 0) > 0 ||
|
||||
(env.compose?.length ?? 0) > 0 ||
|
||||
(env.libsql?.length ?? 0) > 0 ||
|
||||
(env.mariadb?.length ?? 0) > 0 ||
|
||||
(env.mongo?.length ?? 0) > 0 ||
|
||||
(env.mysql?.length ?? 0) > 0 ||
|
||||
(env.postgres?.length ?? 0) > 0 ||
|
||||
(env.redis?.length ?? 0) > 0
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteEnvironment = async (environmentId: string) => {
|
||||
const currentEnvironment = await findEnvironmentById(environmentId);
|
||||
if (currentEnvironment.name === "production") {
|
||||
if (currentEnvironment.isDefault) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "You cannot delete the production environment",
|
||||
message: "You cannot delete the default environment",
|
||||
});
|
||||
}
|
||||
if (environmentHasServices(currentEnvironment)) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Cannot delete environment: it has active services. Delete all services first.",
|
||||
});
|
||||
}
|
||||
const deletedEnvironment = await db
|
||||
@@ -105,7 +280,7 @@ export const updateEnvironmentById = async (
|
||||
};
|
||||
|
||||
export const duplicateEnvironment = async (
|
||||
input: typeof apiDuplicateEnvironment._type,
|
||||
input: z.infer<typeof apiDuplicateEnvironment>,
|
||||
) => {
|
||||
// Find the original environment
|
||||
const originalEnvironment = await findEnvironmentById(input.environmentId);
|
||||
@@ -117,6 +292,7 @@ export const duplicateEnvironment = async (
|
||||
name: input.name,
|
||||
description: input.description || originalEnvironment.description,
|
||||
projectId: originalEnvironment.projectId,
|
||||
env: originalEnvironment.env,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -132,9 +308,23 @@ export const duplicateEnvironment = async (
|
||||
};
|
||||
|
||||
export const createProductionEnvironment = async (projectId: string) => {
|
||||
return createEnvironment({
|
||||
name: "production",
|
||||
description: "Production environment",
|
||||
projectId,
|
||||
});
|
||||
const newEnvironment = await db
|
||||
.insert(environments)
|
||||
.values({
|
||||
name: "production",
|
||||
description: "Production environment",
|
||||
projectId,
|
||||
isDefault: true,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newEnvironment) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the production environment",
|
||||
});
|
||||
}
|
||||
|
||||
return newEnvironment;
|
||||
};
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Gitea = typeof gitea.$inferSelect;
|
||||
|
||||
export const createGitea = async (
|
||||
input: typeof apiCreateGitea._type,
|
||||
input: z.infer<typeof apiCreateGitea>,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { updatePreviewDeployment } from "./preview-deployment";
|
||||
|
||||
export type Github = typeof github.$inferSelect;
|
||||
export const createGithub = async (
|
||||
input: typeof apiCreateGithub._type,
|
||||
input: z.infer<typeof apiCreateGithub>,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Gitlab = typeof gitlab.$inferSelect;
|
||||
|
||||
export const createGitlab = async (
|
||||
input: typeof apiCreateGitlab._type,
|
||||
input: z.infer<typeof apiCreateGitlab>,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
|
||||
162
packages/server/src/services/libsql.ts
Normal file
162
packages/server/src/services/libsql.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateLibsql,
|
||||
backups,
|
||||
buildAppName,
|
||||
libsql,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildLibsql } from "@dokploy/server/utils/databases/libsql";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Libsql = typeof libsql.$inferSelect;
|
||||
|
||||
export const createLibsql = async (input: z.infer<typeof apiCreateLibsql>) => {
|
||||
const appName = buildAppName("libsql", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newLibsql = await db
|
||||
.insert(libsql)
|
||||
.values({
|
||||
...input,
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newLibsql) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting libsql database",
|
||||
});
|
||||
}
|
||||
|
||||
return newLibsql;
|
||||
};
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const findLibsqlById = async (libsqlId: string) => {
|
||||
const result = await db.query.libsql.findFirst({
|
||||
where: eq(libsql.libsqlId, libsqlId),
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
mounts: true,
|
||||
server: true,
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Libsql not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateLibsqlById = async (
|
||||
libsqlId: string,
|
||||
libsqlData: Partial<Libsql>,
|
||||
) => {
|
||||
const { appName, ...rest } = libsqlData;
|
||||
const result = await db
|
||||
.update(libsql)
|
||||
.set({
|
||||
...rest,
|
||||
})
|
||||
.where(eq(libsql.libsqlId, libsqlId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeLibsqlById = async (libsqlId: string) => {
|
||||
const result = await db
|
||||
.delete(libsql)
|
||||
.where(eq(libsql.libsqlId, libsqlId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findLibsqlByBackupId = async (backupId: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
...getTableColumns(libsql),
|
||||
})
|
||||
.from(libsql)
|
||||
.innerJoin(backups, eq(libsql.libsqlId, backups.libsqlId))
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Libsql not found",
|
||||
});
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployLibsql = async (
|
||||
libsqlId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const libsql = await findLibsqlById(libsqlId);
|
||||
try {
|
||||
await updateLibsqlById(libsqlId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
onData?.("Starting libsql deployment...");
|
||||
if (libsql.serverId) {
|
||||
await execAsyncRemote(
|
||||
libsql.serverId,
|
||||
`docker pull ${libsql.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(libsql.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildLibsql(libsql);
|
||||
await updateLibsqlById(libsqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateLibsqlById(libsqlId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error on deploy libsql${error}`,
|
||||
});
|
||||
}
|
||||
return libsql;
|
||||
};
|
||||
@@ -11,14 +11,17 @@ import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Mariadb = typeof mariadb.$inferSelect;
|
||||
|
||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||
export const createMariadb = async (
|
||||
input: z.infer<typeof apiCreateMariaDB>,
|
||||
) => {
|
||||
const appName = buildAppName("mariadb", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
|
||||
@@ -12,11 +12,12 @@ import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Mongo = typeof mongo.$inferSelect;
|
||||
|
||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
export const createMongo = async (input: z.infer<typeof apiCreateMongo>) => {
|
||||
const appName = buildAppName("mongo", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, type SQL, sql } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Mount = typeof mounts.$inferSelect;
|
||||
|
||||
export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
export const createMount = async (input: z.infer<typeof apiCreateMount>) => {
|
||||
try {
|
||||
const { serviceId, ...rest } = input;
|
||||
const value = await db
|
||||
@@ -31,8 +32,11 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
...(input.serviceType === "application" && {
|
||||
applicationId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "postgres" && {
|
||||
postgresId: serviceId,
|
||||
...(input.serviceType === "compose" && {
|
||||
composeId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "libsql" && {
|
||||
libsqlId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "mariadb" && {
|
||||
mariadbId: serviceId,
|
||||
@@ -43,12 +47,12 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
...(input.serviceType === "mysql" && {
|
||||
mysqlId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "postgres" && {
|
||||
postgresId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "redis" && {
|
||||
redisId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "compose" && {
|
||||
composeId: serviceId,
|
||||
}),
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -114,7 +118,16 @@ export const findMountById = async (mountId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
compose: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
@@ -150,7 +163,7 @@ export const findMountById = async (mountId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
postgres: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
@@ -159,7 +172,7 @@ export const findMountById = async (mountId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
redis: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
@@ -185,8 +198,11 @@ export const findMountOrganizationId = async (mountId: string) => {
|
||||
if (mount.application) {
|
||||
return mount.application.environment.project.organizationId;
|
||||
}
|
||||
if (mount.postgres) {
|
||||
return mount.postgres.environment.project.organizationId;
|
||||
if (mount.compose) {
|
||||
return mount.compose.environment.project.organizationId;
|
||||
}
|
||||
if (mount.libsql) {
|
||||
return mount.libsql.environment.project.organizationId;
|
||||
}
|
||||
if (mount.mariadb) {
|
||||
return mount.mariadb.environment.project.organizationId;
|
||||
@@ -197,13 +213,13 @@ export const findMountOrganizationId = async (mountId: string) => {
|
||||
if (mount.mysql) {
|
||||
return mount.mysql.environment.project.organizationId;
|
||||
}
|
||||
if (mount.postgres) {
|
||||
return mount.postgres.environment.project.organizationId;
|
||||
}
|
||||
if (mount.redis) {
|
||||
return mount.redis.environment.project.organizationId;
|
||||
}
|
||||
|
||||
if (mount.compose) {
|
||||
return mount.compose.environment.project.organizationId;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -247,8 +263,8 @@ export const findMountsByApplicationId = async (
|
||||
case "application":
|
||||
sqlChunks.push(eq(mounts.applicationId, serviceId));
|
||||
break;
|
||||
case "postgres":
|
||||
sqlChunks.push(eq(mounts.postgresId, serviceId));
|
||||
case "libsql":
|
||||
sqlChunks.push(eq(mounts.libsqlId, serviceId));
|
||||
break;
|
||||
case "mariadb":
|
||||
sqlChunks.push(eq(mounts.mariadbId, serviceId));
|
||||
@@ -259,9 +275,15 @@ export const findMountsByApplicationId = async (
|
||||
case "mysql":
|
||||
sqlChunks.push(eq(mounts.mysqlId, serviceId));
|
||||
break;
|
||||
case "postgres":
|
||||
sqlChunks.push(eq(mounts.postgresId, serviceId));
|
||||
break;
|
||||
case "redis":
|
||||
sqlChunks.push(eq(mounts.redisId, serviceId));
|
||||
break;
|
||||
case "compose":
|
||||
sqlChunks.push(eq(mounts.composeId, serviceId));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown service type: ${serviceType}`);
|
||||
}
|
||||
@@ -358,6 +380,10 @@ export const getBaseFilesPath = async (mountId: string) => {
|
||||
const { COMPOSE_PATH } = paths(!!mount.compose.serverId);
|
||||
appName = mount.compose.appName;
|
||||
absoluteBasePath = path.resolve(COMPOSE_PATH);
|
||||
} else if (mount.serviceType === "libsql" && mount.libsql) {
|
||||
const { APPLICATIONS_PATH } = paths(!!mount.libsql.serverId);
|
||||
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
||||
appName = mount.libsql.appName;
|
||||
}
|
||||
directoryPath = path.join(absoluteBasePath, appName, "files");
|
||||
|
||||
@@ -387,6 +413,9 @@ export const getServerId = async (mount: MountNested) => {
|
||||
if (mount.serviceType === "compose" && mount?.compose?.serverId) {
|
||||
return mount.compose.serverId;
|
||||
}
|
||||
if (mount.serviceType === "libsql" && mount?.libsql?.serverId) {
|
||||
return mount.libsql.serverId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -11,11 +11,12 @@ import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type MySql = typeof mysql.$inferSelect;
|
||||
|
||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||
export const createMysql = async (input: z.infer<typeof apiCreateMySql>) => {
|
||||
const appName = buildAppName("mysql", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
|
||||
@@ -1,32 +1,48 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateCustom,
|
||||
type apiCreateDiscord,
|
||||
type apiCreateEmail,
|
||||
type apiCreateGotify,
|
||||
type apiCreateLark,
|
||||
type apiCreateNtfy,
|
||||
type apiCreatePushover,
|
||||
type apiCreateResend,
|
||||
type apiCreateSlack,
|
||||
type apiCreateTeams,
|
||||
type apiCreateTelegram,
|
||||
type apiUpdateCustom,
|
||||
type apiUpdateDiscord,
|
||||
type apiUpdateEmail,
|
||||
type apiUpdateGotify,
|
||||
type apiUpdateLark,
|
||||
type apiUpdateNtfy,
|
||||
type apiUpdatePushover,
|
||||
type apiUpdateResend,
|
||||
type apiUpdateSlack,
|
||||
type apiUpdateTeams,
|
||||
type apiUpdateTelegram,
|
||||
custom,
|
||||
discord,
|
||||
email,
|
||||
gotify,
|
||||
lark,
|
||||
notifications,
|
||||
ntfy,
|
||||
pushover,
|
||||
resend,
|
||||
slack,
|
||||
teams,
|
||||
telegram,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Notification = typeof notifications.$inferSelect;
|
||||
|
||||
export const createSlackNotification = async (
|
||||
input: typeof apiCreateSlack._type,
|
||||
input: z.infer<typeof apiCreateSlack>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -54,6 +70,7 @@ export const createSlackNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "slack",
|
||||
@@ -75,7 +92,7 @@ export const createSlackNotification = async (
|
||||
};
|
||||
|
||||
export const updateSlackNotification = async (
|
||||
input: typeof apiUpdateSlack._type,
|
||||
input: z.infer<typeof apiUpdateSlack>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
@@ -85,6 +102,7 @@ export const updateSlackNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -116,7 +134,7 @@ export const updateSlackNotification = async (
|
||||
};
|
||||
|
||||
export const createTelegramNotification = async (
|
||||
input: typeof apiCreateTelegram._type,
|
||||
input: z.infer<typeof apiCreateTelegram>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -145,6 +163,7 @@ export const createTelegramNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "telegram",
|
||||
@@ -166,7 +185,7 @@ export const createTelegramNotification = async (
|
||||
};
|
||||
|
||||
export const updateTelegramNotification = async (
|
||||
input: typeof apiUpdateTelegram._type,
|
||||
input: z.infer<typeof apiUpdateTelegram>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
@@ -176,6 +195,7 @@ export const updateTelegramNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -208,7 +228,7 @@ export const updateTelegramNotification = async (
|
||||
};
|
||||
|
||||
export const createDiscordNotification = async (
|
||||
input: typeof apiCreateDiscord._type,
|
||||
input: z.infer<typeof apiCreateDiscord>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -236,6 +256,7 @@ export const createDiscordNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "discord",
|
||||
@@ -257,7 +278,7 @@ export const createDiscordNotification = async (
|
||||
};
|
||||
|
||||
export const updateDiscordNotification = async (
|
||||
input: typeof apiUpdateDiscord._type,
|
||||
input: z.infer<typeof apiUpdateDiscord>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
@@ -267,6 +288,7 @@ export const updateDiscordNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -298,7 +320,7 @@ export const updateDiscordNotification = async (
|
||||
};
|
||||
|
||||
export const createEmailNotification = async (
|
||||
input: typeof apiCreateEmail._type,
|
||||
input: z.infer<typeof apiCreateEmail>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -330,6 +352,7 @@ export const createEmailNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "email",
|
||||
@@ -351,7 +374,7 @@ export const createEmailNotification = async (
|
||||
};
|
||||
|
||||
export const updateEmailNotification = async (
|
||||
input: typeof apiUpdateEmail._type,
|
||||
input: z.infer<typeof apiUpdateEmail>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
@@ -361,6 +384,7 @@ export const updateEmailNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -395,8 +419,102 @@ export const updateEmailNotification = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const createResendNotification = async (
|
||||
input: z.infer<typeof apiCreateResend>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newResend = await tx
|
||||
.insert(resend)
|
||||
.values({
|
||||
apiKey: input.apiKey,
|
||||
fromAddress: input.fromAddress,
|
||||
toAddresses: input.toAddresses,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newResend) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting resend",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
resendId: newResend.resendId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "resend",
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateResendNotification = async (
|
||||
input: z.infer<typeof apiUpdateResend>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error Updating notification",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(resend)
|
||||
.set({
|
||||
apiKey: input.apiKey,
|
||||
fromAddress: input.fromAddress,
|
||||
toAddresses: input.toAddresses,
|
||||
})
|
||||
.where(eq(resend.resendId, input.resendId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const createGotifyNotification = async (
|
||||
input: typeof apiCreateGotify._type,
|
||||
input: z.infer<typeof apiCreateGotify>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -426,6 +544,7 @@ export const createGotifyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "gotify",
|
||||
@@ -446,7 +565,7 @@ export const createGotifyNotification = async (
|
||||
};
|
||||
|
||||
export const updateGotifyNotification = async (
|
||||
input: typeof apiUpdateGotify._type,
|
||||
input: z.infer<typeof apiUpdateGotify>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
@@ -456,6 +575,7 @@ export const updateGotifyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -486,7 +606,7 @@ export const updateGotifyNotification = async (
|
||||
};
|
||||
|
||||
export const createNtfyNotification = async (
|
||||
input: typeof apiCreateNtfy._type,
|
||||
input: z.infer<typeof apiCreateNtfy>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -495,7 +615,7 @@ export const createNtfyNotification = async (
|
||||
.values({
|
||||
serverUrl: input.serverUrl,
|
||||
topic: input.topic,
|
||||
accessToken: input.accessToken,
|
||||
accessToken: input.accessToken ?? null,
|
||||
priority: input.priority,
|
||||
})
|
||||
.returning()
|
||||
@@ -516,6 +636,7 @@ export const createNtfyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "ntfy",
|
||||
@@ -536,7 +657,7 @@ export const createNtfyNotification = async (
|
||||
};
|
||||
|
||||
export const updateNtfyNotification = async (
|
||||
input: typeof apiUpdateNtfy._type,
|
||||
input: z.infer<typeof apiUpdateNtfy>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
@@ -546,6 +667,7 @@ export const updateNtfyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -566,7 +688,7 @@ export const updateNtfyNotification = async (
|
||||
.set({
|
||||
serverUrl: input.serverUrl,
|
||||
topic: input.topic,
|
||||
accessToken: input.accessToken,
|
||||
accessToken: input.accessToken ?? null,
|
||||
priority: input.priority,
|
||||
})
|
||||
.where(eq(ntfy.ntfyId, input.ntfyId));
|
||||
@@ -575,6 +697,95 @@ export const updateNtfyNotification = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const createCustomNotification = async (
|
||||
input: z.infer<typeof apiCreateCustom>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newCustom = await tx
|
||||
.insert(custom)
|
||||
.values({
|
||||
endpoint: input.endpoint,
|
||||
headers: input.headers,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newCustom) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting custom",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
customId: newCustom.customId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "custom",
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateCustomNotification = async (
|
||||
input: z.infer<typeof apiUpdateCustom>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error Updating notification",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(custom)
|
||||
.set({
|
||||
endpoint: input.endpoint,
|
||||
headers: input.headers,
|
||||
})
|
||||
.where(eq(custom.customId, input.customId));
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const findNotificationById = async (notificationId: string) => {
|
||||
const notification = await db.query.notifications.findFirst({
|
||||
where: eq(notifications.notificationId, notificationId),
|
||||
@@ -583,8 +794,13 @@ export const findNotificationById = async (notificationId: string) => {
|
||||
telegram: true,
|
||||
discord: true,
|
||||
email: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
if (!notification) {
|
||||
@@ -605,6 +821,185 @@ export const removeNotificationById = async (notificationId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const createLarkNotification = async (
|
||||
input: z.infer<typeof apiCreateLark>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newLark = await tx
|
||||
.insert(lark)
|
||||
.values({
|
||||
webhookUrl: input.webhookUrl,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newLark) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting lark",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
larkId: newLark.larkId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "lark",
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateLarkNotification = async (
|
||||
input: z.infer<typeof apiUpdateLark>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error Updating notification",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(lark)
|
||||
.set({
|
||||
webhookUrl: input.webhookUrl,
|
||||
})
|
||||
.where(eq(lark.larkId, input.larkId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const createTeamsNotification = async (
|
||||
input: z.infer<typeof apiCreateTeams>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newTeams = await tx
|
||||
.insert(teams)
|
||||
.values({
|
||||
webhookUrl: input.webhookUrl,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newTeams) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting teams",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
teamsId: newTeams.teamsId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "teams",
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateTeamsNotification = async (
|
||||
input: z.infer<typeof apiUpdateTeams>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error Updating notification",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(teams)
|
||||
.set({
|
||||
webhookUrl: input.webhookUrl,
|
||||
})
|
||||
.where(eq(teams.teamsId, input.teamsId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateNotificationById = async (
|
||||
notificationId: string,
|
||||
notificationData: Partial<Notification>,
|
||||
@@ -619,3 +1014,99 @@ export const updateNotificationById = async (
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const createPushoverNotification = async (
|
||||
input: z.infer<typeof apiCreatePushover>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newPushover = await tx
|
||||
.insert(pushover)
|
||||
.values({
|
||||
userKey: input.userKey,
|
||||
apiToken: input.apiToken,
|
||||
priority: input.priority,
|
||||
retry: input.retry,
|
||||
expire: input.expire,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newPushover) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting pushover",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
pushoverId: newPushover.pushoverId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
serverThreshold: input.serverThreshold,
|
||||
notificationType: "pushover",
|
||||
organizationId: organizationId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updatePushoverNotification = async (
|
||||
input: z.infer<typeof apiUpdatePushover>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error Updating notification",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(pushover)
|
||||
.set({
|
||||
userKey: input.userKey,
|
||||
apiToken: input.apiToken,
|
||||
priority: input.priority,
|
||||
retry: input.retry,
|
||||
expire: input.expire,
|
||||
})
|
||||
.where(eq(pushover.pushoverId, input.pushoverId));
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
197
packages/server/src/services/patch-repo.ts
Normal file
197
packages/server/src/services/patch-repo.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
import { cloneBitbucketRepository } from "../utils/providers/bitbucket";
|
||||
import { cloneGitRepository } from "../utils/providers/git";
|
||||
import { cloneGiteaRepository } from "../utils/providers/gitea";
|
||||
import { cloneGithubRepository } from "../utils/providers/github";
|
||||
import { cloneGitlabRepository } from "../utils/providers/gitlab";
|
||||
import { findApplicationById } from "./application";
|
||||
import { findComposeById } from "./compose";
|
||||
|
||||
interface PatchRepoConfig {
|
||||
type: "application" | "compose";
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure patch repo exists and is up-to-date
|
||||
* Returns path to the repo
|
||||
*/
|
||||
export const ensurePatchRepo = async ({
|
||||
type,
|
||||
id,
|
||||
}: PatchRepoConfig): Promise<string> => {
|
||||
let serverId: string | null = null;
|
||||
|
||||
if (type === "application") {
|
||||
const application = await findApplicationById(id);
|
||||
serverId = application.buildServerId || application.serverId;
|
||||
} else {
|
||||
const compose = await findComposeById(id);
|
||||
serverId = compose.serverId;
|
||||
}
|
||||
|
||||
const application =
|
||||
type === "application"
|
||||
? await findApplicationById(id)
|
||||
: await findComposeById(id);
|
||||
|
||||
const { PATCH_REPOS_PATH } = paths(!!serverId);
|
||||
const repoPath = join(PATCH_REPOS_PATH, type, application.appName);
|
||||
|
||||
const applicationEntity = {
|
||||
...application,
|
||||
type,
|
||||
serverId: serverId,
|
||||
outputPathOverride: repoPath,
|
||||
};
|
||||
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await cloneGithubRepository(applicationEntity);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
command += await cloneGitlabRepository(applicationEntity);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
command += await cloneGiteaRepository(applicationEntity);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
command += await cloneBitbucketRepository(applicationEntity);
|
||||
} else if (application.sourceType === "git") {
|
||||
command += await cloneGitRepository(applicationEntity);
|
||||
}
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
return repoPath;
|
||||
};
|
||||
|
||||
interface DirectoryEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
type: "file" | "directory";
|
||||
children?: DirectoryEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read directory tree of the patch repo
|
||||
*/
|
||||
export const readPatchRepoDirectory = async (
|
||||
repoPath: string,
|
||||
serverId?: string | null,
|
||||
): Promise<DirectoryEntry[]> => {
|
||||
// Use git ls-tree to get tracked files only
|
||||
const command = `cd "${repoPath}" && git ls-tree -r --name-only HEAD`;
|
||||
|
||||
let stdout: string;
|
||||
try {
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Failed to read repository: ${error}`,
|
||||
});
|
||||
}
|
||||
|
||||
const files = stdout.trim().split("\n").filter(Boolean);
|
||||
|
||||
// Build tree structure
|
||||
const root: DirectoryEntry[] = [];
|
||||
const dirMap = new Map<string, DirectoryEntry>();
|
||||
|
||||
for (const filePath of files) {
|
||||
const parts = filePath.split("/");
|
||||
let currentPath = "";
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (!part) continue;
|
||||
|
||||
const isFile = i === parts.length - 1;
|
||||
const parentPath = currentPath;
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
|
||||
if (!dirMap.has(currentPath)) {
|
||||
const entry: DirectoryEntry = {
|
||||
name: part,
|
||||
path: currentPath,
|
||||
type: isFile ? "file" : "directory",
|
||||
children: isFile ? undefined : [],
|
||||
};
|
||||
|
||||
dirMap.set(currentPath, entry);
|
||||
|
||||
if (parentPath) {
|
||||
const parent = dirMap.get(parentPath);
|
||||
parent?.children?.push(entry);
|
||||
} else {
|
||||
root.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
};
|
||||
|
||||
export const readPatchRepoFile = async (
|
||||
id: string,
|
||||
type: "application" | "compose",
|
||||
filePath: string,
|
||||
) => {
|
||||
let serverId: string | null = null;
|
||||
|
||||
if (type === "application") {
|
||||
const application = await findApplicationById(id);
|
||||
serverId = application.buildServerId || application.serverId;
|
||||
} else {
|
||||
const compose = await findComposeById(id);
|
||||
serverId = compose.serverId;
|
||||
}
|
||||
const { PATCH_REPOS_PATH } = paths(!!serverId);
|
||||
|
||||
const application =
|
||||
type === "application"
|
||||
? await findApplicationById(id)
|
||||
: await findComposeById(id);
|
||||
|
||||
const repoPath = join(PATCH_REPOS_PATH, type, application.appName);
|
||||
const fullPath = join(repoPath, filePath);
|
||||
|
||||
const command = `cat "${fullPath}"`;
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
const result = await execAsync(command);
|
||||
return result.stdout;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean all patch repos
|
||||
*/
|
||||
export const cleanPatchRepos = async (
|
||||
serverId?: string | null,
|
||||
): Promise<void> => {
|
||||
const { PATCH_REPOS_PATH } = paths(!!serverId);
|
||||
|
||||
const command = `rm -rf "${PATCH_REPOS_PATH}"/* 2>/dev/null || true`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
176
packages/server/src/services/patch.ts
Normal file
176
packages/server/src/services/patch.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreatePatch, patch } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { findApplicationById } from "./application";
|
||||
import { findComposeById } from "./compose";
|
||||
|
||||
export type Patch = typeof patch.$inferSelect;
|
||||
|
||||
export const createPatch = async (input: z.infer<typeof apiCreatePatch>) => {
|
||||
if (!input.applicationId && !input.composeId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Either applicationId or composeId must be provided",
|
||||
});
|
||||
}
|
||||
|
||||
const newPatch = await db
|
||||
.insert(patch)
|
||||
.values({
|
||||
...input,
|
||||
content: input.content,
|
||||
enabled: true,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newPatch) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the patch",
|
||||
});
|
||||
}
|
||||
|
||||
return newPatch;
|
||||
};
|
||||
|
||||
export const findPatchById = async (patchId: string) => {
|
||||
const result = await db.query.patch.findFirst({
|
||||
where: eq(patch.patchId, patchId),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Patch not found",
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const findPatchesByEntityId = async (
|
||||
id: string,
|
||||
type: "application" | "compose",
|
||||
) => {
|
||||
return await db.query.patch.findMany({
|
||||
where: eq(
|
||||
type === "application" ? patch.applicationId : patch.composeId,
|
||||
id,
|
||||
),
|
||||
orderBy: (patch, { asc }) => [asc(patch.filePath)],
|
||||
});
|
||||
};
|
||||
|
||||
export const findPatchByFilePath = async (
|
||||
filePath: string,
|
||||
id: string,
|
||||
type: "application" | "compose",
|
||||
) => {
|
||||
return await db.query.patch.findFirst({
|
||||
where: and(
|
||||
eq(patch.filePath, filePath),
|
||||
eq(type === "application" ? patch.applicationId : patch.composeId, id),
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export const updatePatch = async (patchId: string, data: Partial<Patch>) => {
|
||||
const result = await db
|
||||
.update(patch)
|
||||
.set({
|
||||
...data,
|
||||
...(data.content && {
|
||||
content: data.content.endsWith("\n")
|
||||
? data.content
|
||||
: `${data.content}\n`,
|
||||
}),
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(patch.patchId, patchId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deletePatch = async (patchId: string) => {
|
||||
const result = await db
|
||||
.delete(patch)
|
||||
.where(eq(patch.patchId, patchId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const markPatchForDeletion = async (
|
||||
filePath: string,
|
||||
entityId: string,
|
||||
entityType: "application" | "compose",
|
||||
) => {
|
||||
const existing = await findPatchByFilePath(filePath, entityId, entityType);
|
||||
|
||||
if (existing) {
|
||||
return await updatePatch(existing.patchId, { type: "delete", content: "" });
|
||||
}
|
||||
|
||||
return await createPatch({
|
||||
filePath,
|
||||
content: "",
|
||||
type: "delete",
|
||||
applicationId: entityType === "application" ? entityId : undefined,
|
||||
composeId: entityType === "compose" ? entityId : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
interface ApplyPatchesOptions {
|
||||
id: string;
|
||||
type: "application" | "compose";
|
||||
serverId: string | null;
|
||||
}
|
||||
|
||||
export const generateApplyPatchesCommand = async ({
|
||||
id,
|
||||
type,
|
||||
serverId,
|
||||
}: ApplyPatchesOptions) => {
|
||||
const entity =
|
||||
type === "application"
|
||||
? await findApplicationById(id)
|
||||
: await findComposeById(id);
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
||||
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const codePath = join(basePath, entity.appName, "code");
|
||||
|
||||
const resultPatches = await findPatchesByEntityId(id, type);
|
||||
const patches = resultPatches.filter((p) => p.enabled);
|
||||
|
||||
if (patches.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let command = `echo "Applying ${patches.length} patch(es)...";`;
|
||||
|
||||
for (const p of patches) {
|
||||
const filePath = join(codePath, p.filePath);
|
||||
|
||||
if (p.type === "delete") {
|
||||
command += `
|
||||
rm -f "${filePath}";
|
||||
`;
|
||||
} else {
|
||||
command += `
|
||||
file="${filePath}"
|
||||
dir="$(dirname "$file")"
|
||||
mkdir -p "$dir"
|
||||
echo "${encodeBase64(p.content)}" | base64 -d > "$file"
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
431
packages/server/src/services/permission.ts
Normal file
431
packages/server/src/services/permission.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { member, organizationRole } from "@dokploy/server/db/schema";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
ac,
|
||||
adminRole,
|
||||
enterpriseOnlyResources,
|
||||
memberRole,
|
||||
ownerRole,
|
||||
statements,
|
||||
} from "../lib/access-control";
|
||||
|
||||
type Statements = typeof statements;
|
||||
type Resource = keyof Statements;
|
||||
type Action<R extends Resource> = Statements[R][number];
|
||||
type Permissions = {
|
||||
[R in Resource]?: Action<R>[];
|
||||
};
|
||||
|
||||
export type PermissionCtx = {
|
||||
user: { id: string };
|
||||
session: { activeOrganizationId: string };
|
||||
};
|
||||
|
||||
export type ResolvedPermissions = {
|
||||
[R in Resource]: {
|
||||
[A in Statements[R][number]]: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const staticRoles: Record<string, ReturnType<typeof ac.newRole>> = {
|
||||
owner: ownerRole,
|
||||
admin: adminRole,
|
||||
member: memberRole,
|
||||
};
|
||||
|
||||
const resolveRole = async (
|
||||
roleName: string,
|
||||
organizationId: string,
|
||||
): Promise<ReturnType<typeof ac.newRole> | null> => {
|
||||
if (staticRoles[roleName]) {
|
||||
return staticRoles[roleName];
|
||||
}
|
||||
|
||||
const licensed = await hasValidLicense(organizationId);
|
||||
if (!licensed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const customRoles = await db.query.organizationRole.findMany({
|
||||
where: and(
|
||||
eq(organizationRole.organizationId, organizationId),
|
||||
eq(organizationRole.role, roleName),
|
||||
),
|
||||
});
|
||||
|
||||
if (customRoles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const merged: Record<string, string[]> = {};
|
||||
for (const entry of customRoles) {
|
||||
const parsed = JSON.parse(entry.permission) as Record<string, string[]>;
|
||||
for (const [resource, actions] of Object.entries(parsed)) {
|
||||
merged[resource] = [
|
||||
...new Set([...(merged[resource] ?? []), ...actions]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ac.newRole(merged as any);
|
||||
};
|
||||
|
||||
export const checkPermission = async (
|
||||
ctx: PermissionCtx,
|
||||
permissions: Permissions,
|
||||
) => {
|
||||
const { id: userId } = ctx.user;
|
||||
const { activeOrganizationId: organizationId } = ctx.session;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
const isStaticRole = memberRecord.role in staticRoles;
|
||||
|
||||
if (isStaticRole) {
|
||||
const allEnterprise = Object.keys(permissions).every((r) =>
|
||||
enterpriseOnlyResources.has(r),
|
||||
);
|
||||
if (allEnterprise) return;
|
||||
}
|
||||
|
||||
const role = await resolveRole(memberRecord.role, organizationId);
|
||||
|
||||
if (!role) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Invalid role",
|
||||
});
|
||||
}
|
||||
|
||||
const result = role.authorize(permissions);
|
||||
if (result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (memberRecord.role === "member") {
|
||||
const overrides = getLegacyOverrides(memberRecord);
|
||||
const allGranted = Object.entries(permissions).every(
|
||||
([resource, actions]) =>
|
||||
(actions as string[]).every(
|
||||
(action) =>
|
||||
!!(overrides[resource] as Record<string, boolean> | undefined)?.[
|
||||
action
|
||||
],
|
||||
),
|
||||
);
|
||||
if (allGranted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: result.error || "Permission denied",
|
||||
});
|
||||
};
|
||||
|
||||
export const hasPermission = async (
|
||||
ctx: PermissionCtx,
|
||||
permissions: Permissions,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await checkPermission(ctx, permissions);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getLegacyOverrides = (
|
||||
memberRecord: Awaited<ReturnType<typeof findMemberByUserId>>,
|
||||
): Partial<Record<string, Record<string, boolean>>> => {
|
||||
return {
|
||||
project: {
|
||||
create: !!memberRecord.canCreateProjects,
|
||||
delete: !!memberRecord.canDeleteProjects,
|
||||
},
|
||||
service: {
|
||||
create: !!memberRecord.canCreateServices,
|
||||
delete: !!memberRecord.canDeleteServices,
|
||||
},
|
||||
environment: {
|
||||
create: !!memberRecord.canCreateEnvironments,
|
||||
delete: !!memberRecord.canDeleteEnvironments,
|
||||
},
|
||||
traefikFiles: {
|
||||
read: !!memberRecord.canAccessToTraefikFiles,
|
||||
},
|
||||
docker: {
|
||||
read: !!memberRecord.canAccessToDocker,
|
||||
},
|
||||
api: {
|
||||
read: !!memberRecord.canAccessToAPI,
|
||||
},
|
||||
sshKeys: {
|
||||
read: !!memberRecord.canAccessToSSHKeys,
|
||||
},
|
||||
gitProviders: {
|
||||
read: !!memberRecord.canAccessToGitProviders,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const resolvePermissions = async (
|
||||
ctx: PermissionCtx,
|
||||
): Promise<ResolvedPermissions> => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
const role = await resolveRole(memberRecord.role, organizationId);
|
||||
|
||||
const legacyOverrides =
|
||||
memberRecord.role === "member" ? getLegacyOverrides(memberRecord) : {};
|
||||
|
||||
const isPrivilegedRole =
|
||||
memberRecord.role === "owner" || memberRecord.role === "admin";
|
||||
const result = {} as ResolvedPermissions;
|
||||
|
||||
for (const [resource, actions] of Object.entries(statements)) {
|
||||
const resourcePerms = {} as Record<string, boolean>;
|
||||
for (const action of actions) {
|
||||
if (isPrivilegedRole && enterpriseOnlyResources.has(resource)) {
|
||||
resourcePerms[action] = true;
|
||||
continue;
|
||||
}
|
||||
if (!role) {
|
||||
resourcePerms[action] = false;
|
||||
continue;
|
||||
}
|
||||
const check = role.authorize({ [resource]: [action] });
|
||||
resourcePerms[action] =
|
||||
check.success ||
|
||||
!!(legacyOverrides[resource] as Record<string, boolean> | undefined)?.[
|
||||
action
|
||||
];
|
||||
}
|
||||
(result as any)[resource] = resourcePerms;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const checkProjectAccess = async (
|
||||
ctx: PermissionCtx,
|
||||
action: "create" | "delete",
|
||||
projectId?: string,
|
||||
) => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
|
||||
await checkPermission(ctx, { project: [action] });
|
||||
|
||||
if (
|
||||
action !== "create" &&
|
||||
projectId &&
|
||||
memberRecord.role !== "owner" &&
|
||||
memberRecord.role !== "admin"
|
||||
) {
|
||||
if (!memberRecord.accessedProjects.includes(projectId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const checkServicePermissionAndAccess = async (
|
||||
ctx: PermissionCtx,
|
||||
serviceId: string,
|
||||
permissions: Permissions,
|
||||
) => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
await checkPermission(ctx, permissions);
|
||||
if (memberRecord.role !== "owner" && memberRecord.role !== "admin") {
|
||||
if (!memberRecord.accessedServices.includes(serviceId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this service",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const checkServiceAccess = async (
|
||||
ctx: PermissionCtx,
|
||||
serviceId: string,
|
||||
action: "create" | "read" | "delete" = "read",
|
||||
) => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
|
||||
await checkPermission(ctx, { service: [action] });
|
||||
|
||||
if (memberRecord.role !== "owner" && memberRecord.role !== "admin") {
|
||||
if (action === "create") {
|
||||
if (!memberRecord.accessedProjects.includes(serviceId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!memberRecord.accessedServices.includes(serviceId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this service",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const checkEnvironmentAccess = async (
|
||||
ctx: PermissionCtx,
|
||||
environmentId: string,
|
||||
action: "read" | "create" | "delete" = "read",
|
||||
) => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
|
||||
await checkPermission(ctx, { environment: [action] });
|
||||
|
||||
if (
|
||||
action !== "create" &&
|
||||
memberRecord.role !== "owner" &&
|
||||
memberRecord.role !== "admin"
|
||||
) {
|
||||
if (!memberRecord.accessedEnvironments.includes(environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this environment",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const checkEnvironmentCreationPermission = async (
|
||||
ctx: PermissionCtx,
|
||||
projectId: string,
|
||||
) => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
|
||||
await checkPermission(ctx, { environment: ["create"] });
|
||||
|
||||
if (memberRecord.role !== "owner" && memberRecord.role !== "admin") {
|
||||
if (!memberRecord.accessedProjects.includes(projectId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const checkEnvironmentDeletionPermission = async (
|
||||
ctx: PermissionCtx,
|
||||
projectId: string,
|
||||
) => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
|
||||
await checkPermission(ctx, { environment: ["delete"] });
|
||||
|
||||
if (memberRecord.role !== "owner" && memberRecord.role !== "admin") {
|
||||
if (!memberRecord.accessedProjects.includes(projectId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const addNewProject = async (ctx: PermissionCtx, projectId: string) => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
await db
|
||||
.update(member)
|
||||
.set({
|
||||
accessedProjects: [...memberRecord.accessedProjects, projectId],
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(member.id, memberRecord.id),
|
||||
eq(member.organizationId, organizationId),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const addNewEnvironment = async (
|
||||
ctx: PermissionCtx,
|
||||
environmentId: string,
|
||||
) => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
await db
|
||||
.update(member)
|
||||
.set({
|
||||
accessedEnvironments: [
|
||||
...memberRecord.accessedEnvironments,
|
||||
environmentId,
|
||||
],
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(member.id, memberRecord.id),
|
||||
eq(member.organizationId, organizationId),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const addNewService = async (ctx: PermissionCtx, serviceId: string) => {
|
||||
const userId = ctx.user.id;
|
||||
const organizationId = ctx.session.activeOrganizationId;
|
||||
const memberRecord = await findMemberByUserId(userId, organizationId);
|
||||
await db
|
||||
.update(member)
|
||||
.set({
|
||||
accessedServices: [...memberRecord.accessedServices, serviceId],
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(member.id, memberRecord.id),
|
||||
eq(member.organizationId, organizationId),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const findMemberByUserId = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const result = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, userId),
|
||||
eq(member.organizationId, organizationId),
|
||||
),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Permission denied",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -2,10 +2,11 @@ import { db } from "@dokploy/server/db";
|
||||
import { type apiCreatePort, ports } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Port = typeof ports.$inferSelect;
|
||||
|
||||
export const createPort = async (input: typeof apiCreatePort._type) => {
|
||||
export const createPort = async (input: z.infer<typeof apiCreatePort>) => {
|
||||
const newPort = await db
|
||||
.insert(ports)
|
||||
.values({
|
||||
|
||||
@@ -11,11 +11,27 @@ import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export function getMountPath(dockerImage: string): string {
|
||||
const versionMatch = dockerImage.match(/postgres:(\d+)/);
|
||||
|
||||
if (versionMatch?.[1]) {
|
||||
const version = Number.parseInt(versionMatch[1], 10);
|
||||
if (version >= 18) {
|
||||
// PostgreSQL 18+ uses /var/lib/postgresql/{version}/docker as the default PGDATA
|
||||
return `/var/lib/postgresql/${version}/docker`;
|
||||
}
|
||||
}
|
||||
return "/var/lib/postgresql/data";
|
||||
}
|
||||
|
||||
export type Postgres = typeof postgres.$inferSelect;
|
||||
|
||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||
export const createPostgres = async (
|
||||
input: z.infer<typeof apiCreatePostgres>,
|
||||
) => {
|
||||
const appName = buildAppName("postgres", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
|
||||
@@ -7,17 +7,18 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { generatePassword } from "../templates";
|
||||
import { removeService } from "../utils/docker/utils";
|
||||
import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||
import { manageDomain } from "../utils/traefik/domain";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
||||
import { createDomain } from "./domain";
|
||||
import { type Github, getIssueComment } from "./github";
|
||||
import { getWebServerSettings } from "./web-server-settings";
|
||||
|
||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||
|
||||
@@ -130,7 +131,7 @@ export const findPreviewDeploymentsByApplicationId = async (
|
||||
};
|
||||
|
||||
export const createPreviewDeployment = async (
|
||||
schema: typeof apiCreatePreviewDeployment._type,
|
||||
schema: z.infer<typeof apiCreatePreviewDeployment>,
|
||||
) => {
|
||||
const application = await findApplicationById(schema.applicationId);
|
||||
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
||||
@@ -235,7 +236,7 @@ const generateWildcardDomain = async (
|
||||
baseDomain: string,
|
||||
appName: string,
|
||||
serverIp: string,
|
||||
userId: string,
|
||||
_userId: string,
|
||||
): Promise<string> => {
|
||||
if (!baseDomain.startsWith("*.")) {
|
||||
throw new Error('The base domain must start with "*."');
|
||||
@@ -253,8 +254,8 @@ const generateWildcardDomain = async (
|
||||
}
|
||||
|
||||
if (!ip) {
|
||||
const admin = await findUserById(userId);
|
||||
ip = admin?.serverIp || "";
|
||||
const settings = await getWebServerSettings();
|
||||
ip = settings?.serverIp || "";
|
||||
}
|
||||
|
||||
const slugIp = ip.replaceAll(".", "-");
|
||||
|
||||
@@ -2,6 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateProject,
|
||||
applications,
|
||||
libsql,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
@@ -11,12 +12,13 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { createProductionEnvironment } from "./environment";
|
||||
|
||||
export type Project = typeof projects.$inferSelect;
|
||||
|
||||
export const createProject = async (
|
||||
input: typeof apiCreateProject._type,
|
||||
input: z.infer<typeof apiCreateProject>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const newProject = await db
|
||||
@@ -52,12 +54,18 @@ export const findProjectById = async (projectId: string) => {
|
||||
environments: {
|
||||
with: {
|
||||
applications: true,
|
||||
compose: true,
|
||||
libsql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
},
|
||||
},
|
||||
projectTags: {
|
||||
with: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -103,6 +111,9 @@ export const validUniqueServerAppName = async (appName: string) => {
|
||||
applications: {
|
||||
where: eq(applications.appName, appName),
|
||||
},
|
||||
libsql: {
|
||||
where: eq(libsql.appName, appName),
|
||||
},
|
||||
mariadb: {
|
||||
where: eq(mariadb.appName, appName),
|
||||
},
|
||||
@@ -125,6 +136,7 @@ export const validUniqueServerAppName = async (appName: string) => {
|
||||
const nonEmptyProjects = query.filter(
|
||||
(project) =>
|
||||
project.applications.length > 0 ||
|
||||
project.libsql.length > 0 ||
|
||||
project.mariadb.length > 0 ||
|
||||
project.mongo.length > 0 ||
|
||||
project.mysql.length > 0 ||
|
||||
|
||||
95
packages/server/src/services/proprietary/audit-log.ts
Normal file
95
packages/server/src/services/proprietary/audit-log.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { auditLog } from "@dokploy/server/db/schema";
|
||||
import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema";
|
||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||
import { and, desc, eq, gte, ilike, lte } from "drizzle-orm";
|
||||
|
||||
export type { AuditAction, AuditResourceType };
|
||||
|
||||
export interface CreateAuditLogInput {
|
||||
organizationId: string;
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
userRole: string;
|
||||
action: AuditAction;
|
||||
resourceType: AuditResourceType;
|
||||
resourceId?: string;
|
||||
resourceName?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an audit log entry. Fire-and-forget safe — errors are swallowed
|
||||
* so a logging failure never breaks the main operation.
|
||||
*/
|
||||
export const createAuditLog = async (input: CreateAuditLogInput) => {
|
||||
try {
|
||||
const licensed = await hasValidLicense(input.organizationId);
|
||||
if (!licensed) return;
|
||||
|
||||
await db.insert(auditLog).values({
|
||||
organizationId: input.organizationId,
|
||||
userId: input.userId,
|
||||
userEmail: input.userEmail,
|
||||
userRole: input.userRole,
|
||||
action: input.action,
|
||||
resourceType: input.resourceType,
|
||||
resourceId: input.resourceId,
|
||||
resourceName: input.resourceName,
|
||||
metadata: input.metadata ? JSON.stringify(input.metadata) : undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[audit-log] Failed to create audit log entry:", err);
|
||||
}
|
||||
};
|
||||
|
||||
export interface GetAuditLogsInput {
|
||||
organizationId: string;
|
||||
userId?: string;
|
||||
userEmail?: string;
|
||||
resourceName?: string;
|
||||
action?: AuditAction;
|
||||
resourceType?: AuditResourceType;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export const getAuditLogs = async (input: GetAuditLogsInput) => {
|
||||
const {
|
||||
organizationId,
|
||||
userId,
|
||||
userEmail,
|
||||
resourceName,
|
||||
action,
|
||||
resourceType,
|
||||
from,
|
||||
to,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
} = input;
|
||||
|
||||
const conditions = [eq(auditLog.organizationId, organizationId)];
|
||||
|
||||
if (userId) conditions.push(eq(auditLog.userId, userId));
|
||||
if (userEmail) conditions.push(ilike(auditLog.userEmail, `%${userEmail}%`));
|
||||
if (resourceName)
|
||||
conditions.push(ilike(auditLog.resourceName, `%${resourceName}%`));
|
||||
if (action) conditions.push(eq(auditLog.action, action));
|
||||
if (resourceType) conditions.push(eq(auditLog.resourceType, resourceType));
|
||||
if (from) conditions.push(gte(auditLog.createdAt, from));
|
||||
if (to) conditions.push(lte(auditLog.createdAt, to));
|
||||
|
||||
const [logs, total] = await Promise.all([
|
||||
db.query.auditLog.findMany({
|
||||
where: and(...conditions),
|
||||
orderBy: [desc(auditLog.createdAt)],
|
||||
limit,
|
||||
offset,
|
||||
}),
|
||||
db.$count(auditLog, and(...conditions)),
|
||||
]);
|
||||
|
||||
return { logs, total };
|
||||
};
|
||||
24
packages/server/src/services/proprietary/license-key.ts
Normal file
24
packages/server/src/services/proprietary/license-key.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { user } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getOrganizationOwnerId } from "./sso";
|
||||
|
||||
export const hasValidLicense = async (organizationId: string) => {
|
||||
const ownerId = await getOrganizationOwnerId(organizationId);
|
||||
|
||||
if (!ownerId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentUser = await db.query.user.findFirst({
|
||||
where: eq(user.id, ownerId),
|
||||
columns: {
|
||||
enableEnterpriseFeatures: true,
|
||||
isValidEnterpriseLicense: true,
|
||||
},
|
||||
});
|
||||
return !!(
|
||||
currentUser?.enableEnterpriseFeatures &&
|
||||
currentUser?.isValidEnterpriseLicense
|
||||
);
|
||||
};
|
||||
46
packages/server/src/services/proprietary/sso.ts
Normal file
46
packages/server/src/services/proprietary/sso.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { organization } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const getSSOProviders = async () => {
|
||||
const providers = await db.query.ssoProvider.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
providerId: true,
|
||||
issuer: true,
|
||||
domain: true,
|
||||
oidcConfig: true,
|
||||
samlConfig: true,
|
||||
},
|
||||
});
|
||||
return providers;
|
||||
};
|
||||
|
||||
export const requestToHeaders = (req: {
|
||||
headers?: Record<string, string | string[] | undefined>;
|
||||
}): Headers => {
|
||||
const headers = new Headers();
|
||||
if (req?.headers) {
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (value !== undefined && key.toLowerCase() !== "host") {
|
||||
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const normalizeTrustedOrigin = (value: string): string => {
|
||||
// Keep it simple: trim and remove trailing slashes.
|
||||
// e.g. "https://example.com/" -> "https://example.com"
|
||||
return value.trim().replace(/\/+$/, "");
|
||||
};
|
||||
|
||||
export const getOrganizationOwnerId = async (organizationId: string) => {
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(organization.id, organizationId),
|
||||
columns: { ownerId: true },
|
||||
});
|
||||
if (!org) return null;
|
||||
return org.ownerId;
|
||||
};
|
||||
@@ -10,12 +10,13 @@ import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Redis = typeof redis.$inferSelect;
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||
export const createRedis = async (input: z.infer<typeof apiCreateRedis>) => {
|
||||
const appName = buildAppName("redis", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
|
||||
export type Registry = typeof registry.$inferSelect;
|
||||
@@ -15,7 +16,7 @@ function shEscape(s: string | undefined): string {
|
||||
return `'${s.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function safeDockerLoginCommand(
|
||||
export function safeDockerLoginCommand(
|
||||
registry: string | undefined,
|
||||
user: string | undefined,
|
||||
pass: string | undefined,
|
||||
@@ -27,7 +28,7 @@ function safeDockerLoginCommand(
|
||||
}
|
||||
|
||||
export const createRegistry = async (
|
||||
input: typeof apiCreateRegistry._type,
|
||||
input: z.infer<typeof apiCreateRegistry>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
deployments as deploymentsSchema,
|
||||
rollbacks,
|
||||
} from "../db/schema";
|
||||
import { type ApplicationNested, getAuthConfig } from "../utils/builders";
|
||||
import type { ApplicationNested } from "../utils/builders";
|
||||
import { getRegistryTag } from "../utils/cluster/upload";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
@@ -22,11 +23,12 @@ import { findDeploymentById } from "./deployment";
|
||||
import type { Mount } from "./mount";
|
||||
import type { Port } from "./port";
|
||||
import type { Project } from "./project";
|
||||
import { type Registry, safeDockerLoginCommand } from "./registry";
|
||||
|
||||
export const createRollback = async (
|
||||
input: z.infer<typeof createRollbackSchema>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const { fullContext, ...other } = input;
|
||||
const rollback = await tx
|
||||
.insert(rollbacks)
|
||||
@@ -70,9 +72,11 @@ export const createRollback = async (
|
||||
})
|
||||
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
|
||||
|
||||
await createRollbackImage(rest, tagImage);
|
||||
const updatedRollback = await tx.query.rollbacks.findFirst({
|
||||
where: eq(rollbacks.rollbackId, rollback.rollbackId),
|
||||
});
|
||||
|
||||
return rollback;
|
||||
return updatedRollback;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -103,32 +107,11 @@ export const findRollbackById = async (rollbackId: string) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const createRollbackImage = async (
|
||||
application: ApplicationNested,
|
||||
tagImage: string,
|
||||
) => {
|
||||
const docker = await getRemoteDocker(application.serverId);
|
||||
|
||||
const appTagName =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: `${application.appName}:latest`;
|
||||
|
||||
const result = docker.getImage(appTagName || "");
|
||||
|
||||
const [repo, version] = tagImage.split(":");
|
||||
|
||||
await result.tag({
|
||||
repo,
|
||||
tag: version,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
|
||||
const command = `docker image rm ${image} --force`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(command, serverId);
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
@@ -179,7 +162,6 @@ export const rollback = async (rollbackId: string) => {
|
||||
if (!result.fullContext) {
|
||||
throw new Error("Rollback context not found");
|
||||
}
|
||||
|
||||
// Use the full context for rollback
|
||||
await rollbackApplication(
|
||||
application.appName,
|
||||
@@ -189,6 +171,23 @@ export const rollback = async (rollbackId: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
const dockerLoginForRegistry = async (
|
||||
registry: Registry,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
const loginCommand = safeDockerLoginCommand(
|
||||
registry.registryUrl,
|
||||
registry.username,
|
||||
registry.password,
|
||||
);
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, loginCommand);
|
||||
} else {
|
||||
await execAsync(loginCommand);
|
||||
}
|
||||
};
|
||||
|
||||
const rollbackApplication = async (
|
||||
appName: string,
|
||||
image: string,
|
||||
@@ -199,12 +198,21 @@ const rollbackApplication = async (
|
||||
};
|
||||
mounts: Mount[];
|
||||
ports: Port[];
|
||||
rollbackRegistry?: Registry;
|
||||
},
|
||||
) => {
|
||||
if (!fullContext) {
|
||||
throw new Error("Full context is required for rollback");
|
||||
}
|
||||
|
||||
// Ensure Docker daemon is authenticated with the rollback registry
|
||||
// before updating the swarm service. The authconfig in CreateServiceOptions
|
||||
// alone is not sufficient — Docker Swarm also relies on the daemon's
|
||||
// cached credentials (~/.docker/config.json) to distribute auth to nodes.
|
||||
if (fullContext.rollbackRegistry) {
|
||||
await dockerLoginForRegistry(fullContext.rollbackRegistry, serverId);
|
||||
}
|
||||
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
|
||||
// Use the same configuration as mechanizeDockerContainer
|
||||
@@ -237,6 +245,7 @@ const rollbackApplication = async (
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(fullContext as ApplicationNested);
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
@@ -245,16 +254,24 @@ const rollbackApplication = async (
|
||||
fullContext.environment.project.env,
|
||||
);
|
||||
|
||||
// For rollback, we use the provided image instead of calculating it
|
||||
const authConfig = getAuthConfig(fullContext as ApplicationNested);
|
||||
// Build the full registry image path if rollbackRegistry is available
|
||||
// e.g., "appName:v5" -> "siumauricio/appName:v5" or "registry.com/prefix/appName:v5"
|
||||
let rollbackImage = image;
|
||||
if (fullContext.rollbackRegistry) {
|
||||
rollbackImage = getRegistryTag(fullContext.rollbackRegistry, image);
|
||||
}
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
authconfig: authConfig,
|
||||
authconfig: {
|
||||
password: fullContext.rollbackRegistry?.password || "",
|
||||
username: fullContext.rollbackRegistry?.username || "",
|
||||
serveraddress: fullContext.rollbackRegistry?.registryUrl || "",
|
||||
},
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Image: rollbackImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount],
|
||||
...(command
|
||||
@@ -263,6 +280,7 @@ const rollbackApplication = async (
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
@@ -297,7 +315,8 @@ const rollbackApplication = async (
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,10 @@ export const createSchedule = async (
|
||||
input: z.infer<typeof createScheduleSchema>,
|
||||
) => {
|
||||
const { scheduleId, ...rest } = input;
|
||||
const [newSchedule] = await db.insert(schedules).values(rest).returning();
|
||||
const [newSchedule] = await db
|
||||
.insert(schedules)
|
||||
.values(rest as typeof schedules.$inferInsert)
|
||||
.returning();
|
||||
|
||||
if (
|
||||
newSchedule &&
|
||||
@@ -120,7 +123,7 @@ export const updateSchedule = async (
|
||||
const { scheduleId, ...rest } = input;
|
||||
const [updatedSchedule] = await db
|
||||
.update(schedules)
|
||||
.set(rest)
|
||||
.set(rest as Partial<typeof schedules.$inferInsert>)
|
||||
.where(eq(schedules.scheduleId, scheduleId))
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ export const createSecurity = async (
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating this security",
|
||||
message:
|
||||
error instanceof Error ? error.message : "Error creating this security",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -90,15 +91,35 @@ export const updateSecurityById = async (
|
||||
data: Partial<Security>,
|
||||
) => {
|
||||
try {
|
||||
const response = await db
|
||||
.update(security)
|
||||
.set({
|
||||
...data,
|
||||
})
|
||||
.where(eq(security.securityId, securityId))
|
||||
.returning();
|
||||
await db.transaction(async (tx) => {
|
||||
const securityResponse = await findSecurityById(securityId);
|
||||
|
||||
return response[0];
|
||||
const application = await findApplicationById(
|
||||
securityResponse.applicationId,
|
||||
);
|
||||
|
||||
await removeSecurityMiddleware(application, securityResponse);
|
||||
|
||||
const response = await tx
|
||||
.update(security)
|
||||
.set({
|
||||
...data,
|
||||
})
|
||||
.where(eq(security.securityId, securityId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!response) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Security not found",
|
||||
});
|
||||
}
|
||||
|
||||
await createSecurityMiddleware(application, response);
|
||||
|
||||
return response;
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Error updating this security";
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export type Server = typeof server.$inferSelect;
|
||||
|
||||
export const createServer = async (
|
||||
input: typeof apiCreateServer._type,
|
||||
input: z.infer<typeof apiCreateServer>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const newServer = await db
|
||||
@@ -19,7 +20,7 @@ export const createServer = async (
|
||||
...input,
|
||||
organizationId: organizationId,
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
} as typeof server.$inferInsert)
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
@@ -79,11 +80,12 @@ export const haveActiveServices = async (serverId: string) => {
|
||||
with: {
|
||||
applications: true,
|
||||
compose: true,
|
||||
redis: true,
|
||||
libsql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -94,11 +96,12 @@ export const haveActiveServices = async (serverId: string) => {
|
||||
const total =
|
||||
currentServer?.applications?.length +
|
||||
currentServer?.compose?.length +
|
||||
currentServer?.redis?.length +
|
||||
currentServer?.libsql?.length +
|
||||
currentServer?.mariadb?.length +
|
||||
currentServer?.mongo?.length +
|
||||
currentServer?.mysql?.length +
|
||||
currentServer?.postgres?.length;
|
||||
currentServer?.postgres?.length +
|
||||
currentServer?.redis?.length;
|
||||
|
||||
if (total === 0) {
|
||||
return false;
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { docker } from "@dokploy/server/constants";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
import semver from "semver";
|
||||
import { db } from "../db";
|
||||
import { compose } from "../db/schema";
|
||||
import {
|
||||
initializeStandaloneTraefik,
|
||||
initializeTraefikService,
|
||||
type TraefikOptions,
|
||||
} from "../setup/traefik-setup";
|
||||
|
||||
export interface IUpdateData {
|
||||
latestVersion: string | null;
|
||||
updateAvailable: boolean;
|
||||
@@ -26,19 +29,6 @@ export const getDokployImageTag = () => {
|
||||
return process.env.RELEASE_TAG || "latest";
|
||||
};
|
||||
|
||||
export const getDokployImage = () => {
|
||||
return `dokploy/dokploy:${getDokployImageTag()}`;
|
||||
};
|
||||
|
||||
export const pullLatestRelease = async () => {
|
||||
const stream = await docker.pull(getDokployImage());
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.modem.followProgress(stream, (err, res) =>
|
||||
err ? reject(err) : resolve(res),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/** Returns Dokploy docker service image digest */
|
||||
export const getServiceImageDigest = async () => {
|
||||
const { stdout } = await execAsync(
|
||||
@@ -55,58 +45,95 @@ export const getServiceImageDigest = async () => {
|
||||
};
|
||||
|
||||
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
|
||||
export const getUpdateData = async (): Promise<IUpdateData> => {
|
||||
let currentDigest: string;
|
||||
export const getUpdateData = async (
|
||||
currentVersion: string,
|
||||
): Promise<IUpdateData> => {
|
||||
try {
|
||||
currentDigest = await getServiceImageDigest();
|
||||
} catch {
|
||||
// Docker service might not exist locally
|
||||
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
|
||||
// https://docs.dokploy.com/docs/core/manual-installation
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
const baseUrl =
|
||||
"https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
|
||||
let url: string | null = `${baseUrl}?page_size=100`;
|
||||
let allResults: { digest: string; name: string }[] = [];
|
||||
|
||||
const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
|
||||
let url: string | null = `${baseUrl}?page_size=100`;
|
||||
let allResults: { digest: string; name: string }[] = [];
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
// Fetch all tags from Docker Hub
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const data = (await response.json()) as {
|
||||
next: string | null;
|
||||
results: { digest: string; name: string }[];
|
||||
};
|
||||
const data = (await response.json()) as {
|
||||
next: string | null;
|
||||
results: { digest: string; name: string }[];
|
||||
};
|
||||
|
||||
allResults = allResults.concat(data.results);
|
||||
url = data?.next;
|
||||
}
|
||||
allResults = allResults.concat(data.results);
|
||||
url = data?.next;
|
||||
}
|
||||
|
||||
const imageTag = getDokployImageTag();
|
||||
const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
|
||||
const currentImageTag = getDokployImageTag();
|
||||
|
||||
if (!searchedDigest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
// Special handling for canary and feature branches
|
||||
// For development versions (canary/feature), don't perform update checks
|
||||
// These are unstable versions that change frequently, and users on these
|
||||
// branches are expected to manually manage updates
|
||||
if (currentImageTag === "canary" || currentImageTag === "feature") {
|
||||
const currentDigest = await getServiceImageDigest();
|
||||
const latestDigest = allResults.find(
|
||||
(t) => t.name === currentImageTag,
|
||||
)?.digest;
|
||||
if (!latestDigest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
if (currentDigest !== latestDigest) {
|
||||
return {
|
||||
latestVersion: currentImageTag,
|
||||
updateAvailable: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
latestVersion: currentImageTag,
|
||||
updateAvailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (imageTag === "latest") {
|
||||
const versionedTag = allResults.find(
|
||||
(t) => t.digest === searchedDigest && t.name.startsWith("v"),
|
||||
);
|
||||
// For stable versions, use semver comparison
|
||||
// Find the "latest" tag and get its digest
|
||||
const latestTag = allResults.find((t) => t.name === "latest");
|
||||
|
||||
if (!versionedTag) {
|
||||
if (!latestTag) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const { name: latestVersion, digest } = versionedTag;
|
||||
const updateAvailable = digest !== currentDigest;
|
||||
// Find the versioned tag (v0.x.x) that has the same digest as "latest"
|
||||
const latestVersionTag = allResults.find(
|
||||
(t) => t.digest === latestTag.digest && t.name.startsWith("v"),
|
||||
);
|
||||
|
||||
return { latestVersion, updateAvailable };
|
||||
if (!latestVersionTag) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const latestVersion = latestVersionTag.name;
|
||||
|
||||
// Use semver to compare versions for stable releases
|
||||
const cleanedCurrent = semver.clean(currentVersion);
|
||||
const cleanedLatest = semver.clean(latestVersion);
|
||||
|
||||
if (!cleanedCurrent || !cleanedLatest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
// Check if the latest version is greater than the current version
|
||||
const updateAvailable = semver.gt(cleanedLatest, cleanedCurrent);
|
||||
|
||||
return {
|
||||
latestVersion,
|
||||
updateAvailable,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching update data:", error);
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
const updateAvailable = searchedDigest !== currentDigest;
|
||||
return { latestVersion: imageTag, updateAvailable };
|
||||
};
|
||||
|
||||
interface TreeDataItem {
|
||||
@@ -217,38 +244,6 @@ echo "$json_output"
|
||||
return result;
|
||||
};
|
||||
|
||||
export const cleanupFullDocker = async (serverId?: string | null) => {
|
||||
const cleanupImages = "docker image prune --force";
|
||||
const cleanupVolumes = "docker volume prune --force";
|
||||
const cleanupContainers = "docker container prune --force";
|
||||
const cleanupSystem = "docker system prune --force --volumes";
|
||||
const cleanupBuilder = "docker builder prune --force";
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`
|
||||
${cleanupImages}
|
||||
${cleanupVolumes}
|
||||
${cleanupContainers}
|
||||
${cleanupSystem}
|
||||
${cleanupBuilder}
|
||||
`,
|
||||
);
|
||||
}
|
||||
await execAsync(`
|
||||
${cleanupImages}
|
||||
${cleanupVolumes}
|
||||
${cleanupContainers}
|
||||
${cleanupSystem}
|
||||
${cleanupBuilder}
|
||||
`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDockerResourceType = async (
|
||||
resourceName: string,
|
||||
serverId?: string,
|
||||
@@ -288,11 +283,22 @@ fi`;
|
||||
export const reloadDockerResource = async (
|
||||
resourceName: string,
|
||||
serverId?: string,
|
||||
version?: string,
|
||||
) => {
|
||||
const resourceType = await getDockerResourceType(resourceName, serverId);
|
||||
let command = "";
|
||||
if (resourceType === "service") {
|
||||
command = `docker service update --force ${resourceName}`;
|
||||
if (resourceName === "dokploy") {
|
||||
const currentImageTag = getDokployImageTag();
|
||||
let imageTag = version;
|
||||
if (currentImageTag === "canary" || currentImageTag === "feature") {
|
||||
imageTag = currentImageTag;
|
||||
}
|
||||
|
||||
command = `docker service update --force --image dokploy/dokploy:${imageTag} ${resourceName}`;
|
||||
} else {
|
||||
command = `docker service update --force ${resourceName}`;
|
||||
}
|
||||
} else if (resourceType === "standalone") {
|
||||
command = `docker restart ${resourceName}`;
|
||||
} else {
|
||||
@@ -374,19 +380,27 @@ export const readPorts = async (
|
||||
publishedPort: number;
|
||||
protocol?: string;
|
||||
}[] = [];
|
||||
const seenPorts = new Set<string>();
|
||||
for (const key in parsedResult) {
|
||||
if (Object.hasOwn(parsedResult, key)) {
|
||||
const containerPortMapppings = parsedResult[key];
|
||||
const protocol = key.split("/")[1];
|
||||
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
|
||||
|
||||
containerPortMapppings.forEach((mapping: any) => {
|
||||
ports.push({
|
||||
targetPort: targetPort,
|
||||
publishedPort: Number.parseInt(mapping.HostPort, 10),
|
||||
protocol: protocol,
|
||||
});
|
||||
});
|
||||
// Take only the first mapping to avoid duplicates (IPv4 and IPv6)
|
||||
const firstMapping = containerPortMapppings[0];
|
||||
if (firstMapping) {
|
||||
const publishedPort = Number.parseInt(firstMapping.HostPort, 10);
|
||||
const portKey = `${targetPort}-${publishedPort}-${protocol}`;
|
||||
if (!seenPorts.has(portKey)) {
|
||||
seenPorts.add(portKey);
|
||||
ports.push({
|
||||
targetPort: targetPort,
|
||||
publishedPort: publishedPort,
|
||||
protocol: protocol,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ports.filter(
|
||||
@@ -394,6 +408,49 @@ export const readPorts = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const checkPortInUse = async (
|
||||
port: number,
|
||||
serverId?: string,
|
||||
): Promise<{ isInUse: boolean; conflictingContainer?: string }> => {
|
||||
try {
|
||||
// Check if port is in use by a Docker container
|
||||
const dockerCommand = `docker ps -a --format '{{.Names}}' | grep -v '^dokploy-traefik$' | while read name; do docker port "$name" 2>/dev/null | grep -q ':${port}' && echo "$name" && break; done || true`;
|
||||
const { stdout: dockerOut } = serverId
|
||||
? await execAsyncRemote(serverId, dockerCommand)
|
||||
: await execAsync(dockerCommand);
|
||||
|
||||
const container = dockerOut.trim();
|
||||
|
||||
if (container) {
|
||||
return {
|
||||
isInUse: true,
|
||||
conflictingContainer: `container "${container}"`,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if port is in use by a host-level service (non-Docker)
|
||||
// Dokploy runs inside a container, so we spawn an ephemeral container
|
||||
// with --net=host to share the host's network stack and use nc -z to
|
||||
// check if something is listening on the port
|
||||
const hostCommand = `docker run --rm --net=host busybox sh -c 'nc -z 0.0.0.0 ${port} 2>/dev/null && echo in_use || echo free'`;
|
||||
const { stdout: hostOut } = serverId
|
||||
? await execAsyncRemote(serverId, hostCommand)
|
||||
: await execAsync(hostCommand);
|
||||
|
||||
if (hostOut.includes("in_use")) {
|
||||
return {
|
||||
isInUse: true,
|
||||
conflictingContainer: "a host-level service",
|
||||
};
|
||||
}
|
||||
|
||||
return { isInUse: false };
|
||||
} catch (error) {
|
||||
console.error("Error checking port availability:", error);
|
||||
return { isInUse: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const writeTraefikSetup = async (input: TraefikOptions) => {
|
||||
const resourceType = await getDockerResourceType(
|
||||
"dokploy-traefik",
|
||||
@@ -406,13 +463,40 @@ export const writeTraefikSetup = async (input: TraefikOptions) => {
|
||||
additionalPorts: input.additionalPorts,
|
||||
serverId: input.serverId,
|
||||
});
|
||||
await reconnectServicesToTraefik(input.serverId);
|
||||
} else if (resourceType === "standalone") {
|
||||
await initializeStandaloneTraefik({
|
||||
env: input.env,
|
||||
additionalPorts: input.additionalPorts,
|
||||
serverId: input.serverId,
|
||||
});
|
||||
|
||||
await reconnectServicesToTraefik(input.serverId);
|
||||
} else {
|
||||
throw new Error("Traefik resource type not found");
|
||||
}
|
||||
};
|
||||
|
||||
export const reconnectServicesToTraefik = async (serverId?: string) => {
|
||||
const composeResult = await db.query.compose.findMany({
|
||||
where: and(
|
||||
...(serverId ? [eq(compose.serverId, serverId)] : []),
|
||||
eq(compose.isolatedDeployment, true),
|
||||
),
|
||||
});
|
||||
|
||||
if (!composeResult) {
|
||||
return;
|
||||
}
|
||||
let commands = "";
|
||||
|
||||
for (const compose of composeResult) {
|
||||
commands += `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1\n`;
|
||||
}
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commands);
|
||||
} else {
|
||||
await execAsync(commands);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,8 +8,9 @@ import {
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const createSshKey = async (input: typeof apiCreateSshKey._type) => {
|
||||
export const createSshKey = async (input: z.infer<typeof apiCreateSshKey>) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const sshKey = await tx
|
||||
.insert(sshKeys)
|
||||
@@ -29,7 +30,7 @@ export const createSshKey = async (input: typeof apiCreateSshKey._type) => {
|
||||
};
|
||||
|
||||
export const removeSSHKeyById = async (
|
||||
sshKeyId: (typeof apiRemoveSshKey._type)["sshKeyId"],
|
||||
sshKeyId: z.infer<typeof apiRemoveSshKey>["sshKeyId"],
|
||||
) => {
|
||||
const result = await db
|
||||
.delete(sshKeys)
|
||||
@@ -42,7 +43,7 @@ export const removeSSHKeyById = async (
|
||||
export const updateSSHKeyById = async ({
|
||||
sshKeyId,
|
||||
...input
|
||||
}: typeof apiUpdateSshKey._type) => {
|
||||
}: z.infer<typeof apiUpdateSshKey>) => {
|
||||
const result = await db
|
||||
.update(sshKeys)
|
||||
.set(input)
|
||||
@@ -53,7 +54,7 @@ export const updateSSHKeyById = async ({
|
||||
};
|
||||
|
||||
export const findSSHKeyById = async (
|
||||
sshKeyId: (typeof apiFindOneSshKey._type)["sshKeyId"],
|
||||
sshKeyId: z.infer<typeof apiFindOneSshKey>["sshKeyId"],
|
||||
) => {
|
||||
const sshKey = await db.query.sshKeys.findFirst({
|
||||
where: eq(sshKeys.sshKeyId, sshKeyId),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { apikey, member, users_temp } from "@dokploy/server/db/schema";
|
||||
import { apikey, member, user } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { auth } from "../lib/auth";
|
||||
|
||||
export type User = typeof users_temp.$inferSelect;
|
||||
export type User = typeof user.$inferSelect;
|
||||
|
||||
export const addNewProject = async (
|
||||
userId: string,
|
||||
@@ -163,6 +163,24 @@ export const canPerformAccessEnvironment = async (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canPerformDeleteEnvironment = async (
|
||||
userId: string,
|
||||
projectId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accessedProjects, canDeleteEnvironments } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||
|
||||
if (canDeleteEnvironments && haveAccessToProject) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const canAccessToTraefikFiles = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
@@ -240,6 +258,42 @@ export const checkEnvironmentAccess = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const checkEnvironmentDeletionPermission = async (
|
||||
userId: string,
|
||||
projectId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const member = await findMemberById(userId, organizationId);
|
||||
|
||||
if (!member) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "User not found in organization",
|
||||
});
|
||||
}
|
||||
|
||||
if (member.role === "owner" || member.role === "admin") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!member.canDeleteEnvironments) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have permission to delete environments",
|
||||
});
|
||||
}
|
||||
|
||||
const hasProjectAccess = member.accessedProjects.includes(projectId);
|
||||
if (!hasProjectAccess) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const checkProjectAccess = async (
|
||||
authId: string,
|
||||
action: "create" | "delete" | "access",
|
||||
@@ -272,6 +326,46 @@ export const checkProjectAccess = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const checkEnvironmentCreationPermission = async (
|
||||
userId: string,
|
||||
projectId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
// Get user's member record
|
||||
const member = await findMemberById(userId, organizationId);
|
||||
|
||||
if (!member) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "User not found in organization",
|
||||
});
|
||||
}
|
||||
|
||||
// Owners and admins can always create environments
|
||||
if (member.role === "owner" || member.role === "admin") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has canCreateEnvironments permission
|
||||
if (!member.canCreateEnvironments) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have permission to create environments",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user has access to the project
|
||||
const hasProjectAccess = member.accessedProjects.includes(projectId);
|
||||
if (!hasProjectAccess) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const findMemberById = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
@@ -309,16 +403,16 @@ export const updateUser = async (userId: string, userData: Partial<User>) => {
|
||||
}
|
||||
}
|
||||
|
||||
const user = await db
|
||||
.update(users_temp)
|
||||
const userResult = await db
|
||||
.update(user)
|
||||
.set({
|
||||
...userData,
|
||||
})
|
||||
.where(eq(users_temp.id, userId))
|
||||
.where(eq(user.id, userId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
return user;
|
||||
return userResult;
|
||||
};
|
||||
|
||||
export const createApiKey = async (
|
||||
@@ -338,7 +432,7 @@ export const createApiKey = async (
|
||||
refillInterval?: number;
|
||||
},
|
||||
) => {
|
||||
const apiKey = await auth.createApiKey({
|
||||
const result = await auth.createApiKey({
|
||||
body: {
|
||||
name: input.name,
|
||||
expiresIn: input.expiresIn,
|
||||
@@ -356,10 +450,9 @@ export const createApiKey = async (
|
||||
if (input.metadata) {
|
||||
await db
|
||||
.update(apikey)
|
||||
.set({
|
||||
metadata: JSON.stringify(input.metadata),
|
||||
})
|
||||
.where(eq(apikey.id, apiKey.id));
|
||||
.set({ metadata: JSON.stringify(input.metadata) })
|
||||
.where(eq(apikey.id, result.id));
|
||||
}
|
||||
return apiKey;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -12,13 +12,78 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await db.query.volumeBackups.findFirst({
|
||||
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
||||
with: {
|
||||
application: true,
|
||||
postgres: true,
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
application: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
@@ -38,7 +103,7 @@ export const createVolumeBackup = async (
|
||||
) => {
|
||||
const newVolumeBackup = await db
|
||||
.insert(volumeBackups)
|
||||
.values(volumeBackup)
|
||||
.values(volumeBackup as typeof volumeBackups.$inferInsert)
|
||||
.returning()
|
||||
.then((e) => e[0]);
|
||||
|
||||
@@ -57,7 +122,7 @@ export const updateVolumeBackup = async (
|
||||
) => {
|
||||
return await db
|
||||
.update(volumeBackups)
|
||||
.set(volumeBackup)
|
||||
.set(volumeBackup as Partial<typeof volumeBackups.$inferInsert>)
|
||||
.where(eq(volumeBackups.volumeBackupId, volumeBackupId))
|
||||
.returning()
|
||||
.then((e) => e[0]);
|
||||
|
||||
44
packages/server/src/services/web-server-settings.ts
Normal file
44
packages/server/src/services/web-server-settings.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { webServerSettings } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Get the web server settings (singleton - only one row should exist)
|
||||
*/
|
||||
export const getWebServerSettings = async () => {
|
||||
const settings = await db.query.webServerSettings.findFirst({
|
||||
orderBy: (settings, { asc }) => [asc(settings.createdAt)],
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
// Create default settings if none exist
|
||||
const [newSettings] = await db
|
||||
.insert(webServerSettings)
|
||||
.values({})
|
||||
.returning();
|
||||
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update web server settings
|
||||
*/
|
||||
export const updateWebServerSettings = async (
|
||||
updates: Partial<typeof webServerSettings.$inferInsert>,
|
||||
) => {
|
||||
const current = await getWebServerSettings();
|
||||
|
||||
const [updated] = await db
|
||||
.update(webServerSettings)
|
||||
.set({
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(webServerSettings.id, current?.id ?? ""))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||
import type { ContainerCreateOptions } from "dockerode";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findUserById } from "../services/admin";
|
||||
import { getDokployImageTag } from "../services/settings";
|
||||
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
@@ -83,8 +83,8 @@ export const setupMonitoring = async (serverId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setupWebMonitoring = async (userId: string) => {
|
||||
const user = await findUserById(userId);
|
||||
export const setupWebMonitoring = async () => {
|
||||
const webServerSettings = await getWebServerSettings();
|
||||
|
||||
const containerName = "dokploy-monitoring";
|
||||
let imageName = "dokploy/monitoring:latest";
|
||||
@@ -99,7 +99,7 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
|
||||
const settings: ContainerCreateOptions = {
|
||||
name: containerName,
|
||||
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
|
||||
Env: [`METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`],
|
||||
Image: imageName,
|
||||
HostConfig: {
|
||||
// Memory: 100 * 1024 * 1024, // 100MB en bytes
|
||||
@@ -110,9 +110,9 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
Name: "always",
|
||||
},
|
||||
PortBindings: {
|
||||
[`${user?.metricsConfig?.server?.port}/tcp`]: [
|
||||
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: [
|
||||
{
|
||||
HostPort: user?.metricsConfig?.server?.port.toString(),
|
||||
HostPort: webServerSettings?.metricsConfig?.server?.port.toString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -126,7 +126,7 @@ export const setupWebMonitoring = async (userId: string) => {
|
||||
// NetworkMode: "host",
|
||||
},
|
||||
ExposedPorts: {
|
||||
[`${user?.metricsConfig?.server?.port}/tcp`]: {},
|
||||
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: {},
|
||||
},
|
||||
};
|
||||
const docker = await getRemoteDocker();
|
||||
|
||||
@@ -17,7 +17,7 @@ export const initializePostgres = async () => {
|
||||
Mounts: [
|
||||
{
|
||||
Type: "volume",
|
||||
Source: "dokploy-postgres-database",
|
||||
Source: "dokploy-postgres",
|
||||
Target: "/var/lib/postgresql/data",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -14,7 +14,7 @@ export const initializeRedis = async () => {
|
||||
Mounts: [
|
||||
{
|
||||
Type: "volume",
|
||||
Source: "redis-data-volume",
|
||||
Source: "dokploy-redis",
|
||||
Target: "/data",
|
||||
},
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user