mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge branch 'canary' into feat/add-mattermost-notification-provider
Resolves merge conflicts between mattermost notification provider (this PR) and new canary features (resend, teams, SSO, patches, etc). All notification providers are now included: - slack, telegram, discord, email, gotify, ntfy - mattermost (this PR) - resend, pushover, custom, lark, teams (from canary)
This commit is contained in:
274
packages/server/auth-schema2.ts
Normal file
274
packages/server/auth-schema2.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
// 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(""),
|
||||
// });
|
||||
|
||||
// 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(),
|
||||
// name: text("name"),
|
||||
// start: text("start"),
|
||||
// prefix: text("prefix"),
|
||||
// key: text("key").notNull(),
|
||||
// userId: text("user_id")
|
||||
// .notNull()
|
||||
// .references(() => user.id, { onDelete: "cascade" }),
|
||||
// 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_key_idx").on(table.key),
|
||||
// index("apikey_userId_idx").on(table.userId),
|
||||
// ],
|
||||
// );
|
||||
|
||||
// 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 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),
|
||||
// apikeys: many(apikey),
|
||||
// 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 apikeyRelations = relations(apikey, ({ one }) => ({
|
||||
// user: one(user, {
|
||||
// fields: [apikey.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 }) => ({
|
||||
// members: many(member),
|
||||
// invitations: many(invitation),
|
||||
// }));
|
||||
|
||||
// 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,43 +19,45 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"dbml:generate": "npx tsx src/db/schema/dbml.ts"
|
||||
"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.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/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",
|
||||
"@better-auth/sso": "1.5.0-beta.16",
|
||||
"@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.0-beta.16",
|
||||
"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",
|
||||
"yaml": "2.8.1",
|
||||
"lodash": "4.17.21",
|
||||
@@ -73,22 +75,26 @@
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"resend": "^6.0.2",
|
||||
"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",
|
||||
"semver": "7.7.3"
|
||||
"zod": "^4.3.6",
|
||||
"semver": "7.7.3",
|
||||
"better-call": "1.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "6.1.3",
|
||||
"@better-auth/cli": "1.5.0-beta.13",
|
||||
"@types/semver": "7.7.1",
|
||||
"@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": "^24.4.0",
|
||||
"@types/node-schedule": "2.1.6",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
@@ -97,7 +103,7 @@
|
||||
"@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",
|
||||
@@ -106,9 +112,9 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,14 @@ import path from "node:path";
|
||||
import Docker from "dockerode";
|
||||
|
||||
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
||||
export const CLEANUP_CRON_JOB = "50 23 * * *";
|
||||
export const docker = new 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"
|
||||
@@ -25,5 +31,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`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,7 +26,8 @@ if (DATABASE_URL) {
|
||||
password,
|
||||
)}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`;
|
||||
} else {
|
||||
console.warn(`
|
||||
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.
|
||||
@@ -34,6 +35,13 @@ if (DATABASE_URL) {
|
||||
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
|
||||
`);
|
||||
dbUrl =
|
||||
"postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy";
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
dbUrl =
|
||||
"postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy";
|
||||
} else {
|
||||
dbUrl =
|
||||
"postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +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(dbUrl), {
|
||||
// En producción no usamos global cache
|
||||
dbConnection = drizzle(postgres(dbUrl), {
|
||||
schema,
|
||||
});
|
||||
} else {
|
||||
if (!global.db)
|
||||
global.db = drizzle(postgres(dbUrl), {
|
||||
// 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 };
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { nanoid } from "nanoid";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import { ssoProvider } from "./sso";
|
||||
import { user } from "./user";
|
||||
|
||||
export const account = pgTable("account", {
|
||||
@@ -78,6 +79,7 @@ export const organizationRelations = relations(
|
||||
servers: many(server),
|
||||
projects: many(projects),
|
||||
members: many(member),
|
||||
ssoProviders: many(ssoProvider),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -153,6 +155,7 @@ export const invitation = pgTable("invitation", {
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
teamId: text("team_id"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const invitationRelations = relations(invitation, ({ one }) => ({
|
||||
|
||||
@@ -19,6 +19,7 @@ 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 { redirects } from "./redirects";
|
||||
@@ -43,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",
|
||||
@@ -156,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
|
||||
@@ -172,6 +175,7 @@ export const applications = pgTable("application", {
|
||||
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")
|
||||
@@ -283,11 +287,17 @@ export const applicationsRelations = relations(
|
||||
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(),
|
||||
@@ -321,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",
|
||||
@@ -359,6 +370,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
cleanCache: z.boolean().optional(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
@@ -369,11 +381,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({
|
||||
@@ -509,11 +519,9 @@ export const apiSaveEnvironmentVariables = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindMonitoringStats = createSchema
|
||||
.pick({
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindMonitoringStats = z.object({
|
||||
appName: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiUpdateApplication = createSchema
|
||||
.partial()
|
||||
|
||||
@@ -165,11 +165,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({
|
||||
|
||||
@@ -12,11 +12,12 @@ import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
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",
|
||||
@@ -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({
|
||||
|
||||
@@ -126,7 +126,6 @@ const schema = createInsertSchema(deployments, {
|
||||
previewDeploymentId: z.string(),
|
||||
buildServerId: z.string(),
|
||||
});
|
||||
|
||||
export const apiCreateDeployment = schema
|
||||
.pick({
|
||||
title: true,
|
||||
@@ -210,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,6 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
@@ -46,46 +45,30 @@ export const environmentRelations = relations(
|
||||
}),
|
||||
);
|
||||
|
||||
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),
|
||||
})
|
||||
.omit({
|
||||
isDefault: true,
|
||||
});
|
||||
|
||||
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";
|
||||
@@ -62,10 +61,6 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ 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 +33,7 @@ export * from "./server";
|
||||
export * from "./session";
|
||||
export * from "./shared";
|
||||
export * from "./ssh-key";
|
||||
export * from "./sso";
|
||||
export * from "./user";
|
||||
export * from "./utils";
|
||||
export * from "./volume-backups";
|
||||
|
||||
@@ -23,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")
|
||||
@@ -67,6 +69,7 @@ export const mariadb = pgTable("mariadb", {
|
||||
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()
|
||||
@@ -96,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),
|
||||
@@ -136,28 +144,25 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
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({
|
||||
|
||||
@@ -30,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")
|
||||
@@ -70,6 +72,7 @@ export const mongo = pgTable("mongo", {
|
||||
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()
|
||||
@@ -98,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),
|
||||
@@ -133,27 +141,24 @@ const createSchema = createInsertSchema(mongo, {
|
||||
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({
|
||||
|
||||
@@ -130,11 +130,9 @@ export const apiCreateMount = createSchema
|
||||
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({
|
||||
@@ -148,12 +146,20 @@ export const apiRemoveMount = createSchema
|
||||
export const apiFindMountByApplicationId = createSchema
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
serviceType: z.enum([
|
||||
"application",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
]),
|
||||
})
|
||||
.pick({
|
||||
serviceId: true,
|
||||
serviceType: true,
|
||||
})
|
||||
.required();
|
||||
});
|
||||
|
||||
export const apiUpdateMount = createSchema.partial().extend({
|
||||
mountId: z.string().min(1),
|
||||
|
||||
@@ -23,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")
|
||||
@@ -65,6 +67,7 @@ export const mysql = pgTable("mysql", {
|
||||
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()
|
||||
@@ -93,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),
|
||||
@@ -133,28 +141,25 @@ const createSchema = createInsertSchema(mysql, {
|
||||
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({
|
||||
|
||||
@@ -17,12 +17,14 @@ export const notificationType = pgEnum("notificationType", [
|
||||
"telegram",
|
||||
"discord",
|
||||
"email",
|
||||
"resend",
|
||||
"gotify",
|
||||
"ntfy",
|
||||
"mattermost",
|
||||
"pushover",
|
||||
"custom",
|
||||
"lark",
|
||||
"teams",
|
||||
]);
|
||||
|
||||
export const notifications = pgTable("notification", {
|
||||
@@ -54,6 +56,9 @@ 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",
|
||||
}),
|
||||
@@ -72,6 +77,9 @@ export const notifications = pgTable("notification", {
|
||||
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" }),
|
||||
@@ -118,6 +126,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()
|
||||
@@ -179,6 +197,14 @@ export const pushover = pgTable("pushover", {
|
||||
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],
|
||||
@@ -196,6 +222,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],
|
||||
@@ -220,6 +250,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.pushoverId],
|
||||
references: [pushover.pushoverId],
|
||||
}),
|
||||
teams: one(teams, {
|
||||
fields: [notifications.teamsId],
|
||||
references: [teams.teamsId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [notifications.organizationId],
|
||||
references: [organization.id],
|
||||
@@ -353,6 +387,36 @@ 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,
|
||||
@@ -422,6 +486,7 @@ export const apiCreateMattermost = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -438,6 +503,7 @@ export const apiCreateMattermost = notificationsSchema
|
||||
webhookUrl: true,
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
@@ -461,11 +527,9 @@ export const apiTestMattermostConnection = apiCreateMattermost
|
||||
username: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneNotification = notificationsSchema
|
||||
.pick({
|
||||
notificationId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneNotification = z.object({
|
||||
notificationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateCustom = notificationsSchema
|
||||
.pick({
|
||||
@@ -480,7 +544,7 @@ export const apiCreateCustom = notificationsSchema
|
||||
})
|
||||
.extend({
|
||||
endpoint: z.string().min(1),
|
||||
headers: z.record(z.string()).optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateCustom = apiCreateCustom.partial().extend({
|
||||
@@ -491,7 +555,7 @@ export const apiUpdateCustom = apiCreateCustom.partial().extend({
|
||||
|
||||
export const apiTestCustomConnection = z.object({
|
||||
endpoint: z.string().min(1),
|
||||
headers: z.record(z.string()).optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
|
||||
export const apiCreateLark = notificationsSchema
|
||||
@@ -520,6 +584,32 @@ 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,
|
||||
@@ -595,6 +685,7 @@ 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(),
|
||||
|
||||
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({
|
||||
|
||||
@@ -23,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")
|
||||
@@ -65,6 +67,7 @@ export const postgres = pgTable("postgres", {
|
||||
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()
|
||||
@@ -94,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@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
@@ -126,27 +135,24 @@ const createSchema = createInsertSchema(postgres, {
|
||||
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({
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -43,12 +43,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({
|
||||
|
||||
@@ -22,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")
|
||||
@@ -64,6 +66,7 @@ export const redis = pgTable("redis", {
|
||||
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")
|
||||
@@ -88,7 +91,12 @@ 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(),
|
||||
@@ -115,25 +123,22 @@ const createSchema = createInsertSchema(redis, {
|
||||
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({
|
||||
|
||||
@@ -94,11 +94,9 @@ export const apiRemoveRegistry = createSchema
|
||||
})
|
||||
.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),
|
||||
|
||||
@@ -471,6 +471,7 @@ table git_provider {
|
||||
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
|
||||
@@ -497,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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -133,6 +133,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
|
||||
@@ -147,11 +148,9 @@ export const apiCreateServer = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindOneServer = createSchema
|
||||
.pick({
|
||||
serverId: true,
|
||||
})
|
||||
.required();
|
||||
export const apiFindOneServer = z.object({
|
||||
serverId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiRemoveServer = createSchema
|
||||
.pick({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
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(),
|
||||
|
||||
@@ -86,6 +86,14 @@ export interface EndpointSpecSwarm {
|
||||
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(),
|
||||
@@ -167,12 +175,12 @@ 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({
|
||||
@@ -189,3 +197,13 @@ export const EndpointSpecSwarmSchema = z
|
||||
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(),
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import { account, apikey, organization } from "./account";
|
||||
import { backups } from "./backups";
|
||||
import { projects } from "./project";
|
||||
import { schedules } from "./schedule";
|
||||
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.
|
||||
@@ -53,9 +54,18 @@ export const user = pgTable("user", {
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
||||
// Enterprise / proprietary features
|
||||
enableEnterpriseFeatures: boolean("enableEnterpriseFeatures")
|
||||
.notNull()
|
||||
.default(false),
|
||||
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(user, ({ one, many }) => ({
|
||||
@@ -66,6 +76,7 @@ export const usersRelations = relations(user, ({ one, many }) => ({
|
||||
organizations: many(organization),
|
||||
projects: many(projects),
|
||||
apiKeys: many(apikey),
|
||||
ssoProviders: many(ssoProvider),
|
||||
backups: many(backups),
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
@@ -75,6 +86,8 @@ const createSchema = createInsertSchema(user, {
|
||||
isRegistered: z.boolean().optional(),
|
||||
}).omit({
|
||||
role: true,
|
||||
trustedOrigins: true,
|
||||
isValidEnterpriseLicense: true,
|
||||
});
|
||||
|
||||
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
||||
|
||||
@@ -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, "-");
|
||||
|
||||
@@ -27,10 +27,14 @@ 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";
|
||||
@@ -79,6 +83,7 @@ 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";
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
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 { 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 {
|
||||
getTrustedOrigins,
|
||||
getTrustedProviders,
|
||||
getUserByToken,
|
||||
} from "../services/admin";
|
||||
import {
|
||||
getWebServerSettings,
|
||||
updateWebServerSettings,
|
||||
@@ -22,6 +27,37 @@ const { handler, api } = betterAuth({
|
||||
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: {
|
||||
@@ -36,24 +72,27 @@ const { handler, api } = betterAuth({
|
||||
logger: {
|
||||
disabled: process.env.NODE_ENV === "production",
|
||||
},
|
||||
...(!IS_CLOUD && {
|
||||
async trustedOrigins() {
|
||||
const settings = await getWebServerSettings();
|
||||
if (!settings) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
||||
...(settings?.host ? [`https://${settings?.host}`] : []),
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? [
|
||||
"http://localhost:3000",
|
||||
"https://absolutely-handy-falcon.ngrok-free.app",
|
||||
]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
}),
|
||||
async trustedOrigins() {
|
||||
const trustedOrigins = await getTrustedOrigins();
|
||||
if (IS_CLOUD) {
|
||||
return trustedOrigins;
|
||||
}
|
||||
const settings = await getWebServerSettings();
|
||||
if (!settings) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
||||
...(settings?.host ? [`https://${settings?.host}`] : []),
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? [
|
||||
"http://localhost:3000",
|
||||
"https://absolutely-handy-falcon.ngrok-free.app",
|
||||
]
|
||||
: []),
|
||||
...trustedOrigins,
|
||||
];
|
||||
},
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
autoSignInAfterVerification: true,
|
||||
@@ -106,6 +145,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"),
|
||||
});
|
||||
@@ -118,6 +161,7 @@ const { handler, api } = betterAuth({
|
||||
}
|
||||
},
|
||||
after: async (user, context) => {
|
||||
const isSSORequest = context?.path.includes("/sso");
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
});
|
||||
@@ -173,6 +217,29 @@ const { handler, api } = betterAuth({
|
||||
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,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -234,12 +301,23 @@ const { handler, api } = betterAuth({
|
||||
input: true,
|
||||
defaultValue: "",
|
||||
},
|
||||
enableEnterpriseFeatures: {
|
||||
type: "boolean",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
isValidEnterpriseLicense: {
|
||||
type: "boolean",
|
||||
required: false,
|
||||
input: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
apiKey({
|
||||
enableMetadata: true,
|
||||
}),
|
||||
sso(),
|
||||
twoFactor(),
|
||||
organization({
|
||||
async sendInvitationEmail(data, _request) {
|
||||
@@ -270,11 +348,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) {
|
||||
@@ -286,7 +369,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 {
|
||||
@@ -352,6 +435,8 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
twoFactorEnabled: userFromDb.twoFactorEnabled,
|
||||
role: member?.role || "member",
|
||||
ownerId: member?.organization.ownerId || apiKeyRecord.user.id,
|
||||
enableEnterpriseFeatures: userFromDb.enableEnterpriseFeatures,
|
||||
isValidEnterpriseLicense: userFromDb.isValidEnterpriseLicense,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -383,17 +468,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 {
|
||||
|
||||
@@ -116,3 +116,31 @@ export const getDokployUrl = async () => {
|
||||
}
|
||||
return `http://${settings?.serverIp}:${process.env.PORT}`;
|
||||
};
|
||||
|
||||
export const getTrustedOrigins = async () => {
|
||||
const members = await db.query.member.findMany({
|
||||
where: eq(member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (members.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const trustedOrigins = members.flatMap(
|
||||
(member) => member.user.trustedOrigins || [],
|
||||
);
|
||||
|
||||
return Array.from(new Set(trustedOrigins));
|
||||
};
|
||||
|
||||
export const getTrustedProviders = async () => {
|
||||
try {
|
||||
const providers = await db.query.ssoProvider.findMany();
|
||||
return providers.map((provider) => provider.providerId);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,13 +2,31 @@ 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 { 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({
|
||||
where: eq(ai.organizationId, organizationId),
|
||||
@@ -60,7 +78,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const suggestVariants = async ({
|
||||
organizationId,
|
||||
organizationId: _organizationId,
|
||||
aiId,
|
||||
input,
|
||||
serverId,
|
||||
@@ -90,173 +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 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 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"}]
|
||||
}
|
||||
|
||||
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
|
||||
Note: configFiles is optional - only include it if configuration files are absolutely required.
|
||||
|
||||
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)
|
||||
Follow these rules:
|
||||
|
||||
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}
|
||||
`,
|
||||
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,
|
||||
|
||||
@@ -29,6 +29,7 @@ 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 {
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
issueCommentExists,
|
||||
updateIssueComment,
|
||||
} from "./github";
|
||||
import { generateApplyPatchesCommand } from "./patch";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
@@ -52,7 +54,7 @@ import { validUniqueServerAppName } from "./project";
|
||||
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);
|
||||
|
||||
@@ -174,6 +176,10 @@ export const deployApplication = async ({
|
||||
}) => {
|
||||
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({
|
||||
@@ -185,19 +191,27 @@ export const deployApplication = async ({
|
||||
try {
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await cloneGithubRepository(application);
|
||||
command += await cloneGithubRepository(applicationEntity);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
command += await cloneGitlabRepository(application);
|
||||
command += await cloneGitlabRepository(applicationEntity);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
command += await cloneGiteaRepository(application);
|
||||
command += await cloneGiteaRepository(applicationEntity);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
command += await cloneBitbucketRepository(application);
|
||||
command += await cloneBitbucketRepository(applicationEntity);
|
||||
} else if (application.sourceType === "git") {
|
||||
command += await cloneGitRepository(application);
|
||||
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`;
|
||||
@@ -258,7 +272,6 @@ export const deployApplication = async ({
|
||||
type: "application",
|
||||
serverId: serverId,
|
||||
});
|
||||
|
||||
if (commitInfo) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
title: commitInfo.message,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,7 @@ 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 {
|
||||
@@ -40,11 +41,14 @@ import {
|
||||
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);
|
||||
@@ -247,8 +251,15 @@ export const deployCompose = async ({
|
||||
} 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) {
|
||||
@@ -395,16 +406,14 @@ 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;
|
||||
|
||||
@@ -13,10 +13,14 @@ import {
|
||||
deployments,
|
||||
} 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 type { z } from "zod";
|
||||
import {
|
||||
type Application,
|
||||
findApplicationById,
|
||||
@@ -69,7 +73,7 @@ export const findDeploymentByApplicationId = async (applicationId: string) => {
|
||||
|
||||
export const createDeployment = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeployment._type,
|
||||
z.infer<typeof apiCreateDeployment>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
@@ -150,7 +154,7 @@ export const createDeployment = async (
|
||||
|
||||
export const createDeploymentPreview = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentPreview._type,
|
||||
z.infer<typeof apiCreateDeploymentPreview>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
@@ -233,7 +237,7 @@ export const createDeploymentPreview = async (
|
||||
|
||||
export const createDeploymentCompose = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentCompose._type,
|
||||
z.infer<typeof apiCreateDeploymentCompose>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
@@ -310,7 +314,7 @@ echo "Initializing deployment\n" >> ${logFilePath};
|
||||
|
||||
export const createDeploymentBackup = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentBackup._type,
|
||||
z.infer<typeof apiCreateDeploymentBackup>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
@@ -390,7 +394,7 @@ echo "Initializing backup\n" >> ${logFilePath};
|
||||
|
||||
export const createDeploymentSchedule = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentSchedule._type,
|
||||
z.infer<typeof apiCreateDeploymentSchedule>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
@@ -466,7 +470,7 @@ export const createDeploymentSchedule = async (
|
||||
|
||||
export const createDeploymentVolumeBackup = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentVolumeBackup._type,
|
||||
z.infer<typeof apiCreateDeploymentVolumeBackup>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
@@ -554,8 +558,25 @@ 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) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Deployment not found",
|
||||
});
|
||||
}
|
||||
const command = `
|
||||
rm -f ${deployment.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";
|
||||
@@ -753,7 +774,7 @@ export const updateDeploymentStatus = async (
|
||||
|
||||
export const createServerDeployment = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentServer._type,
|
||||
z.infer<typeof apiCreateDeploymentServer>,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
@@ -831,3 +852,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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { findApplicationById } from "./application";
|
||||
import { detectCDNProvider } from "./cdn";
|
||||
@@ -13,14 +14,14 @@ 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]);
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -101,6 +102,20 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
||||
return projectEnvironments;
|
||||
};
|
||||
|
||||
const environmentHasServices = (
|
||||
env: Awaited<ReturnType<typeof findEnvironmentById>>,
|
||||
) => {
|
||||
return (
|
||||
(env.applications?.length ?? 0) > 0 ||
|
||||
(env.compose?.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.isDefault) {
|
||||
@@ -109,6 +124,13 @@ export const deleteEnvironment = async (environmentId: string) => {
|
||||
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
|
||||
.delete(environments)
|
||||
.where(eq(environments.environmentId, environmentId))
|
||||
@@ -135,7 +157,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);
|
||||
|
||||
@@ -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,
|
||||
) => {
|
||||
|
||||
@@ -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
|
||||
@@ -262,6 +263,9 @@ export const findMountsByApplicationId = async (
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
type apiCreateMattermost,
|
||||
type apiCreateNtfy,
|
||||
type apiCreatePushover,
|
||||
type apiCreateResend,
|
||||
type apiCreateSlack,
|
||||
type apiCreateTeams,
|
||||
type apiCreateTelegram,
|
||||
type apiUpdateCustom,
|
||||
type apiUpdateDiscord,
|
||||
@@ -18,7 +20,9 @@ import {
|
||||
type apiUpdateMattermost,
|
||||
type apiUpdateNtfy,
|
||||
type apiUpdatePushover,
|
||||
type apiUpdateResend,
|
||||
type apiUpdateSlack,
|
||||
type apiUpdateTeams,
|
||||
type apiUpdateTelegram,
|
||||
custom,
|
||||
discord,
|
||||
@@ -29,16 +33,19 @@ import {
|
||||
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) => {
|
||||
@@ -88,7 +95,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
|
||||
@@ -130,7 +137,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) => {
|
||||
@@ -181,7 +188,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
|
||||
@@ -224,7 +231,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) => {
|
||||
@@ -274,7 +281,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
|
||||
@@ -316,7 +323,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) => {
|
||||
@@ -370,7 +377,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
|
||||
@@ -415,8 +422,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) => {
|
||||
@@ -467,7 +568,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
|
||||
@@ -508,7 +609,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) => {
|
||||
@@ -559,7 +660,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
|
||||
@@ -600,7 +701,7 @@ export const updateNtfyNotification = async (
|
||||
};
|
||||
|
||||
export const createCustomNotification = async (
|
||||
input: typeof apiCreateCustom._type,
|
||||
input: z.infer<typeof apiCreateCustom>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -649,7 +750,7 @@ export const createCustomNotification = async (
|
||||
};
|
||||
|
||||
export const updateCustomNotification = async (
|
||||
input: typeof apiUpdateCustom._type,
|
||||
input: z.infer<typeof apiUpdateCustom>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
@@ -696,12 +797,14 @@ export const findNotificationById = async (notificationId: string) => {
|
||||
telegram: true,
|
||||
discord: true,
|
||||
email: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
if (!notification) {
|
||||
@@ -723,7 +826,7 @@ export const removeNotificationById = async (notificationId: string) => {
|
||||
};
|
||||
|
||||
export const createLarkNotification = async (
|
||||
input: typeof apiCreateLark._type,
|
||||
input: z.infer<typeof apiCreateLark>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -771,7 +874,7 @@ export const createLarkNotification = async (
|
||||
};
|
||||
|
||||
export const updateLarkNotification = async (
|
||||
input: typeof apiUpdateLark._type,
|
||||
input: z.infer<typeof apiUpdateLark>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
@@ -811,6 +914,96 @@ export const updateLarkNotification = async (
|
||||
});
|
||||
};
|
||||
|
||||
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>,
|
||||
@@ -919,7 +1112,7 @@ export const updateMattermostNotification = async (
|
||||
};
|
||||
|
||||
export const createPushoverNotification = async (
|
||||
input: typeof apiCreatePushover._type,
|
||||
input: z.infer<typeof apiCreatePushover>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
@@ -972,7 +1165,7 @@ export const createPushoverNotification = async (
|
||||
};
|
||||
|
||||
export const updatePushoverNotification = async (
|
||||
input: typeof apiUpdatePushover._type,
|
||||
input: z.infer<typeof apiUpdatePushover>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -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,6 +11,7 @@ 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 {
|
||||
@@ -28,7 +29,9 @@ export function getMountPath(dockerImage: string): string {
|
||||
|
||||
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,6 +7,7 @@ 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";
|
||||
@@ -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)}`;
|
||||
|
||||
@@ -11,12 +11,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
|
||||
|
||||
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;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -220,6 +220,7 @@ const rollbackApplication = async (
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(fullContext as ApplicationNested);
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
@@ -254,6 +255,7 @@ const rollbackApplication = async (
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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,
|
||||
@@ -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(
|
||||
@@ -452,13 +442,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),
|
||||
|
||||
@@ -94,7 +94,7 @@ export const createVolumeBackup = async (
|
||||
) => {
|
||||
const newVolumeBackup = await db
|
||||
.insert(volumeBackups)
|
||||
.values(volumeBackup)
|
||||
.values(volumeBackup as typeof volumeBackups.$inferInsert)
|
||||
.returning()
|
||||
.then((e) => e[0]);
|
||||
|
||||
@@ -113,7 +113,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]);
|
||||
|
||||
@@ -281,17 +281,43 @@ const installRequirements = async (
|
||||
.on("error", (err) => {
|
||||
client.end();
|
||||
if (err.level === "client-authentication") {
|
||||
onData?.(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
);
|
||||
const technicalDetail = `Error: ${err.message} ${err.level}`;
|
||||
const friendlyMessage = [
|
||||
"",
|
||||
"❌ Couldn't connect to your server — the SSH key was not accepted.",
|
||||
"",
|
||||
"This usually means the key doesn't match what's on the server, or the key format is invalid.",
|
||||
"",
|
||||
`Technical details: ${technicalDetail}`,
|
||||
"",
|
||||
"💡 Hints:",
|
||||
" • Check that the SSH key you added in Dokploy is the same one installed on the server (e.g. in ~/.ssh/authorized_keys).",
|
||||
" • Try generating a new SSH key in Dokploy and add only the public key to the server, then try again.",
|
||||
" • Make sure to follow the instructions on the Setup Server Button on the SSH Keys tab",
|
||||
].join("\n");
|
||||
onData?.(friendlyMessage);
|
||||
reject(
|
||||
new Error(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
`Authentication failed: Invalid SSH private key. ${technicalDetail}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
onData?.(`SSH connection error: ${err.message} ${err.level}`);
|
||||
reject(new Error(`SSH connection error: ${err.message}`));
|
||||
const technicalDetail = `${err.message} ${err.level ?? ""}`.trim();
|
||||
const friendlyMessage = [
|
||||
"",
|
||||
"❌ Couldn't connect to your server.",
|
||||
"",
|
||||
"The connection failed before setup could run. Common causes: wrong IP or port, firewall blocking access, or the server is offline.",
|
||||
"",
|
||||
`Technical details: ${technicalDetail}`,
|
||||
"",
|
||||
"💡 Hints:",
|
||||
" • Check that the server IP address and SSH port are correct and the server is powered on.",
|
||||
" • If the server is in a private network, ensure Dokploy can reach it (VPN, firewall rules, or correct security groups).",
|
||||
" • Make sure the SSH port (usually 22) is open and the SSH service is running on the server.",
|
||||
].join("\n");
|
||||
onData?.(friendlyMessage);
|
||||
reject(new Error(`SSH connection error: ${technicalDetail}`));
|
||||
}
|
||||
})
|
||||
.connect({
|
||||
|
||||
@@ -20,7 +20,7 @@ export const TRAEFIK_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||
export const TRAEFIK_HTTP3_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
|
||||
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.6.4";
|
||||
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.6.7";
|
||||
|
||||
export interface TraefikOptions {
|
||||
env?: string[];
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import {
|
||||
getWebServerSettings,
|
||||
@@ -12,8 +14,6 @@ export const startLogCleanup = async (
|
||||
cronExpression = "0 0 * * *",
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
|
||||
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
|
||||
if (existingJob) {
|
||||
existingJob.cancel();
|
||||
@@ -21,10 +21,17 @@ export const startLogCleanup = async (
|
||||
|
||||
scheduleJob(LOG_CLEANUP_JOB_NAME, cronExpression, async () => {
|
||||
try {
|
||||
await execAsync(
|
||||
`tail -n 1000 ${DYNAMIC_TRAEFIK_PATH}/access.log > ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp && mv ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp ${DYNAMIC_TRAEFIK_PATH}/access.log`,
|
||||
);
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const accessLogPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
||||
|
||||
if (!fs.existsSync(accessLogPath)) {
|
||||
console.error("Access log file does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
await execAsync(
|
||||
`tail -n 1000 ${accessLogPath} > ${accessLogPath}.tmp && mv ${accessLogPath}.tmp ${accessLogPath}`,
|
||||
);
|
||||
await execAsync("docker exec dokploy-traefik kill -USR1 1");
|
||||
} catch (error) {
|
||||
console.error("Error during log cleanup:", error);
|
||||
|
||||
@@ -103,7 +103,7 @@ export const getProviderHeaders = (
|
||||
// Mistral
|
||||
if (apiUrl.includes("mistral")) {
|
||||
return {
|
||||
Authorization: apiKey,
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { CLEANUP_CRON_JOB } from "@dokploy/server/constants";
|
||||
import { member } from "@dokploy/server/db/schema";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { getAllServers } from "@dokploy/server/services/server";
|
||||
@@ -29,15 +30,19 @@ export const initCronJobs = async () => {
|
||||
const webServerSettings = await getWebServerSettings();
|
||||
|
||||
if (webServerSettings?.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
);
|
||||
try {
|
||||
scheduleJob("docker-cleanup", CLEANUP_CRON_JOB, async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
);
|
||||
|
||||
await cleanupAll();
|
||||
await cleanupAll();
|
||||
|
||||
await sendDockerCleanupNotifications(admin.user.id);
|
||||
});
|
||||
await sendDockerCleanupNotifications(admin.user.id);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Backup] Docker Cleanup Error", error);
|
||||
}
|
||||
}
|
||||
|
||||
const servers = await getAllServers();
|
||||
@@ -45,18 +50,22 @@ export const initCronJobs = async () => {
|
||||
for (const server of servers) {
|
||||
const { serverId, enableDockerCleanup, name } = server;
|
||||
if (enableDockerCleanup) {
|
||||
scheduleJob(serverId, "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
||||
);
|
||||
try {
|
||||
scheduleJob(serverId, CLEANUP_CRON_JOB, async () => {
|
||||
console.log(
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
||||
);
|
||||
|
||||
await cleanupAll(serverId);
|
||||
await cleanupAll(serverId);
|
||||
|
||||
await sendDockerCleanupNotifications(
|
||||
admin.user.id,
|
||||
`Docker cleanup for Server ${name} (${serverId})`,
|
||||
);
|
||||
});
|
||||
await sendDockerCleanupNotifications(
|
||||
admin.user.id,
|
||||
`Docker cleanup for Server ${name} (${serverId})`,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[Backup] ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +95,15 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
|
||||
if (webServerSettings?.logCleanupCron) {
|
||||
console.log(
|
||||
"Starting log requests cleanup",
|
||||
webServerSettings.logCleanupCron,
|
||||
);
|
||||
await startLogCleanup(webServerSettings.logCleanupCron);
|
||||
try {
|
||||
console.log(
|
||||
"Starting log requests cleanup",
|
||||
webServerSettings.logCleanupCron,
|
||||
);
|
||||
await startLogCleanup(webServerSettings.logCleanupCron);
|
||||
} catch (error) {
|
||||
console.error("[Backup] Log Cleanup Error", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import path, { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Application } from "@dokploy/server/services/application";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { readValidDirectory } from "@dokploy/server/wss/utils";
|
||||
import AdmZip from "adm-zip";
|
||||
import { Client, type SFTPWrapper } from "ssh2";
|
||||
import {
|
||||
@@ -16,10 +17,13 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
|
||||
try {
|
||||
const { appName } = application;
|
||||
const { APPLICATIONS_PATH } = paths(!!application.serverId);
|
||||
// Use buildServerId if set, otherwise fall back to serverId
|
||||
// This ensures the code is extracted to the server where the build will run
|
||||
const targetServerId = application.buildServerId || application.serverId;
|
||||
const { APPLICATIONS_PATH } = paths(!!targetServerId);
|
||||
const outputPath = join(APPLICATIONS_PATH, appName, "code");
|
||||
if (application.serverId) {
|
||||
await recreateDirectoryRemote(outputPath, application.serverId);
|
||||
if (targetServerId) {
|
||||
await recreateDirectoryRemote(outputPath, targetServerId);
|
||||
} else {
|
||||
await recreateDirectory(outputPath);
|
||||
}
|
||||
@@ -45,8 +49,8 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
? rootEntries[0]?.entryName.split("/")[0]
|
||||
: "";
|
||||
|
||||
if (application.serverId) {
|
||||
sftp = await getSFTPConnection(application.serverId);
|
||||
if (targetServerId) {
|
||||
sftp = await getSFTPConnection(targetServerId);
|
||||
}
|
||||
for (const entry of zipEntries) {
|
||||
let filePath = entry.entryName;
|
||||
@@ -62,16 +66,24 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
if (!filePath) continue;
|
||||
|
||||
const fullPath = path.join(outputPath, filePath).replace(/\\/g, "/");
|
||||
if (!readValidDirectory(fullPath, application.serverId)) {
|
||||
throw new Error(
|
||||
`Path traversal detected: resolved path escapes output directory: ${filePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (application.serverId) {
|
||||
if (isDangerousNode(entry)) {
|
||||
throw new Error(
|
||||
`Dangerous node entries are not allowed: ${entry.entryName}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (targetServerId) {
|
||||
if (!entry.isDirectory) {
|
||||
if (sftp === null) throw new Error("No SFTP connection available");
|
||||
try {
|
||||
const dirPath = path.dirname(fullPath);
|
||||
await execAsyncRemote(
|
||||
application.serverId,
|
||||
`mkdir -p "${dirPath}"`,
|
||||
);
|
||||
await execAsyncRemote(targetServerId, `mkdir -p "${dirPath}"`);
|
||||
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
||||
} catch (err) {
|
||||
console.error(`Error uploading file ${fullPath}:`, err);
|
||||
@@ -132,3 +144,14 @@ const uploadFileToServer = (
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function isDangerousNode(entry: AdmZip.IZipEntry) {
|
||||
const type = (entry.header.attr >> 16) & 0o170000;
|
||||
|
||||
return (
|
||||
type === 0o120000 || // symlink
|
||||
type === 0o060000 || // block device
|
||||
type === 0o020000 || // char device
|
||||
type === 0o010000 // fifo/pipe
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ export const mechanizeDockerContainer = async (
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(application);
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
@@ -142,7 +143,7 @@ export const mechanizeDockerContainer = async (
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
||||
|
||||
export const createEnvFile = (
|
||||
directory: string,
|
||||
env: string | null,
|
||||
projectEnv?: string | null,
|
||||
environmentEnv?: string | null,
|
||||
) => {
|
||||
const envFilePath = join(dirname(directory), ".env");
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
const envFileContent = prepareEnvironmentVariables(
|
||||
env,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
).join("\n");
|
||||
writeFileSync(envFilePath, envFileContent);
|
||||
};
|
||||
|
||||
export const createEnvFileCommand = (
|
||||
directory: string,
|
||||
env: string | null,
|
||||
|
||||
68
packages/server/src/utils/crons/enterprise.ts
Normal file
68
packages/server/src/utils/crons/enterprise.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getPublicIpWithFallback } from "@dokploy/server/wss/utils";
|
||||
import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import { scheduleJob } from "node-schedule";
|
||||
import { db } from "../../db/index";
|
||||
import { user as userSchema } from "../../db/schema/user";
|
||||
|
||||
export const LICENSE_KEY_URL =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:4002"
|
||||
: "https://licenses-api.dokploy.com";
|
||||
|
||||
export const initEnterpriseBackupCronJobs = async () => {
|
||||
scheduleJob("enterprise-check", "0 0 */3 * *", async () => {
|
||||
const users = await db.query.user.findMany({
|
||||
where: and(
|
||||
isNotNull(userSchema.licenseKey),
|
||||
isNotNull(userSchema.enableEnterpriseFeatures),
|
||||
eq(userSchema.isValidEnterpriseLicense, true),
|
||||
),
|
||||
});
|
||||
for (const user of users) {
|
||||
if (user.isValidEnterpriseLicense) {
|
||||
console.log(
|
||||
"Validating license key....",
|
||||
user.firstName,
|
||||
user.lastName,
|
||||
);
|
||||
try {
|
||||
const isValid = await validateLicenseKey(user.licenseKey || "");
|
||||
if (!isValid) {
|
||||
throw new Error("License key is invalid");
|
||||
}
|
||||
} catch (error) {
|
||||
await db
|
||||
.update(userSchema)
|
||||
.set({ isValidEnterpriseLicense: false })
|
||||
.where(eq(userSchema.id, user.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const validateLicenseKey = async (licenseKey: string) => {
|
||||
try {
|
||||
const ip = await getPublicIpWithFallback();
|
||||
const result = await fetch(`${LICENSE_KEY_URL}/licenses/validate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ licenseKey, ip }),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const errorData = await result.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Failed to validate license key");
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
return data.valid;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
error instanceof Error ? error.message : "Failed to validate license key",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -48,6 +48,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(mariadb);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
@@ -83,7 +84,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -94,6 +94,7 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(mongo);
|
||||
|
||||
const resources = calculateResources({
|
||||
@@ -139,7 +140,7 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
!replicaSets && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -54,6 +54,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(mysql);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
@@ -89,7 +90,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -47,6 +47,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(postgres);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
@@ -82,7 +83,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
args.length > 0 && {
|
||||
Args: args,
|
||||
}),
|
||||
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -45,6 +45,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
Networks,
|
||||
StopGracePeriod,
|
||||
EndpointSpec,
|
||||
Ulimits,
|
||||
} = generateConfigContainer(redis);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
@@ -87,6 +88,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", `redis-server --requirepass ${databasePassword}`],
|
||||
}),
|
||||
...(Ulimits && { Ulimits }),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
|
||||
@@ -164,10 +164,12 @@ export const addDomainToCompose = async (
|
||||
for (const domain of domains) {
|
||||
const { serviceName, https } = domain;
|
||||
if (!serviceName) {
|
||||
throw new Error("Service name not found");
|
||||
throw new Error(`Domain "${domain.host}" is missing a service name`);
|
||||
}
|
||||
if (!result?.services?.[serviceName]) {
|
||||
throw new Error(`The service ${serviceName} not found in the compose`);
|
||||
throw new Error(
|
||||
`Domain "${domain.host}" is attached to service "${serviceName}" which does not exist in the compose`,
|
||||
);
|
||||
}
|
||||
|
||||
const httpLabels = createDomainLabels(appName, domain, "web");
|
||||
@@ -330,6 +332,7 @@ export const addDokployNetworkToService = (
|
||||
) => {
|
||||
let networks = networkService;
|
||||
const network = "dokploy-network";
|
||||
const defaultNetwork = "default";
|
||||
if (!networks) {
|
||||
networks = [];
|
||||
}
|
||||
@@ -338,10 +341,16 @@ export const addDokployNetworkToService = (
|
||||
if (!networks.includes(network)) {
|
||||
networks.push(network);
|
||||
}
|
||||
if (!networks.includes(defaultNetwork)) {
|
||||
networks.push(defaultNetwork);
|
||||
}
|
||||
} else if (networks && typeof networks === "object") {
|
||||
if (!(network in networks)) {
|
||||
networks[network] = {};
|
||||
}
|
||||
if (!(defaultNetwork in networks)) {
|
||||
networks[defaultNetwork] = {};
|
||||
}
|
||||
}
|
||||
|
||||
return networks;
|
||||
|
||||
@@ -508,6 +508,7 @@ export const generateConfigContainer = (
|
||||
networkSwarm,
|
||||
stopGracePeriodSwarm,
|
||||
endpointSpecSwarm,
|
||||
ulimitsSwarm,
|
||||
} = application;
|
||||
|
||||
const sanitizedStopGracePeriodSwarm =
|
||||
@@ -584,6 +585,10 @@ export const generateConfigContainer = (
|
||||
})) || [],
|
||||
},
|
||||
}),
|
||||
...(ulimitsSwarm &&
|
||||
ulimitsSwarm.length > 0 && {
|
||||
Ulimits: ulimitsSwarm,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -102,7 +102,8 @@ export const removeMonitoringDirectory = async (
|
||||
};
|
||||
|
||||
export const getBuildAppDirectory = (application: Application) => {
|
||||
const { APPLICATIONS_PATH } = paths(!!application.serverId);
|
||||
const serverId = application.buildServerId || application.serverId;
|
||||
const { APPLICATIONS_PATH } = paths(!!serverId);
|
||||
const { appName, buildType, sourceType, customGitBuildPath, dockerfile } =
|
||||
application;
|
||||
let buildPath = "";
|
||||
@@ -126,7 +127,7 @@ export const getBuildAppDirectory = (application: Application) => {
|
||||
appName,
|
||||
"code",
|
||||
buildPath ?? "",
|
||||
dockerfile || "",
|
||||
dockerfile || "Dockerfile",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -46,18 +48,21 @@ export const sendBuildErrorNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
@@ -67,9 +72,10 @@ export const sendBuildErrorNotifications = async ({
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
try {
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
BuildFailedEmail({
|
||||
projectName,
|
||||
@@ -80,11 +86,22 @@ export const sendBuildErrorNotifications = async ({
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
).catch();
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Build failed for dokploy",
|
||||
template,
|
||||
);
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Build failed for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Build failed for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -391,6 +408,26 @@ ${errorMessage}
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}\nError: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const limitCharacter = 800;
|
||||
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
||||
await sendTeamsNotification(teams, {
|
||||
title: "⚠️ Build Failed",
|
||||
facts: [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Type", value: applicationType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{ name: "Error Message", value: truncatedErrorMessage },
|
||||
],
|
||||
potentialAction: {
|
||||
type: "Action.OpenUrl",
|
||||
title: "View Build Details",
|
||||
url: buildLink,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -49,18 +51,21 @@ export const sendBuildSuccessNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
@@ -70,9 +75,10 @@ export const sendBuildSuccessNotifications = async ({
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
try {
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
BuildSuccessEmail({
|
||||
projectName,
|
||||
@@ -83,11 +89,22 @@ export const sendBuildSuccessNotifications = async ({
|
||||
environmentName,
|
||||
}),
|
||||
).catch();
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Build success for dokploy",
|
||||
template,
|
||||
);
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Build success for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Build success for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -393,6 +410,24 @@ export const sendBuildSuccessNotifications = async ({
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: "✅ Build Success",
|
||||
facts: [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Environment", value: environmentName },
|
||||
{ name: "Type", value: applicationType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
],
|
||||
potentialAction: {
|
||||
type: "Action.OpenUrl",
|
||||
title: "View Build Details",
|
||||
url: buildLink,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -46,18 +48,21 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
@@ -67,9 +72,10 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
try {
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
DatabaseBackupEmail({
|
||||
projectName,
|
||||
@@ -80,11 +86,22 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
date: date.toLocaleString(),
|
||||
}),
|
||||
).catch();
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Database backup for dokploy",
|
||||
template,
|
||||
);
|
||||
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Database backup for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Database backup for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -414,6 +431,30 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nDatabase: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const facts = [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Database Type", value: databaseType },
|
||||
{ name: "Database Name", value: databaseName },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{
|
||||
name: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
},
|
||||
];
|
||||
if (type === "error" && errorMessage) {
|
||||
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
|
||||
}
|
||||
await sendTeamsNotification(teams, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Database Backup Successful"
|
||||
: "❌ Database Backup Failed",
|
||||
facts,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
@@ -33,18 +35,21 @@ export const sendDockerCleanupNotifications = async (
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const {
|
||||
email,
|
||||
resend,
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
@@ -54,18 +59,29 @@ export const sendDockerCleanupNotifications = async (
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
try {
|
||||
if (email) {
|
||||
if (email || resend) {
|
||||
const template = await renderAsync(
|
||||
DockerCleanupEmail({ message, date: date.toLocaleString() }),
|
||||
).catch();
|
||||
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Docker cleanup for dokploy",
|
||||
template,
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(
|
||||
email,
|
||||
"Docker cleanup for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
|
||||
if (resend) {
|
||||
await sendResendNotification(
|
||||
resend,
|
||||
"Docker cleanup for dokploy",
|
||||
template,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
@@ -260,6 +276,16 @@ export const sendDockerCleanupNotifications = async (
|
||||
`Date: ${date.toLocaleString()}\nMessage: ${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
await sendTeamsNotification(teams, {
|
||||
title: "✅ Docker Cleanup",
|
||||
facts: [
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{ name: "Message", value: message },
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user