Merge branch 'canary' into resend-provider-for-notifications

This commit is contained in:
Mauricio Siu
2026-02-05 14:42:14 -06:00
71 changed files with 12977 additions and 1271 deletions

View File

@@ -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";
}
}

View File

@@ -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),
}),
);

View File

@@ -47,7 +47,7 @@ import {
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",
@@ -287,7 +287,12 @@ export const applicationsRelations = relations(
);
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(),

View File

@@ -16,7 +16,7 @@ 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",
@@ -147,6 +147,12 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
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(),

View File

@@ -32,6 +32,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";

View File

@@ -26,7 +26,7 @@ import {
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")
@@ -96,7 +96,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),
@@ -138,20 +143,18 @@ const createSchema = createInsertSchema(mariadb, {
endpointSpecSwarm: EndpointSpecSwarmSchema.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({

View File

@@ -33,7 +33,7 @@ import {
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")
@@ -98,7 +98,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),
@@ -135,19 +140,17 @@ const createSchema = createInsertSchema(mongo, {
endpointSpecSwarm: EndpointSpecSwarmSchema.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({

View File

@@ -26,7 +26,7 @@ import {
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")
@@ -93,7 +93,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),
@@ -135,20 +140,18 @@ const createSchema = createInsertSchema(mysql, {
endpointSpecSwarm: EndpointSpecSwarmSchema.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({

View File

@@ -26,7 +26,7 @@ import {
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")
@@ -94,6 +94,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@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
@@ -128,19 +134,17 @@ const createSchema = createInsertSchema(postgres, {
endpointSpecSwarm: EndpointSpecSwarmSchema.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({

View File

@@ -25,7 +25,7 @@ import {
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")
@@ -88,7 +88,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(),
@@ -117,17 +122,15 @@ const createSchema = createInsertSchema(redis, {
endpointSpecSwarm: EndpointSpecSwarmSchema.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({

View File

@@ -0,0 +1,132 @@
import { relations } from "drizzle-orm";
import { pgTable, text } 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(),
});
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(),
});

View File

@@ -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({

View File

@@ -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, "-");