Merge branch 'canary' into ulimits-at-0a401843

This commit is contained in:
Mauricio Siu
2026-02-08 23:30:14 -06:00
234 changed files with 69952 additions and 5923 deletions

View File

@@ -0,0 +1,47 @@
import fs from "node:fs";
export const {
DATABASE_URL,
POSTGRES_PASSWORD_FILE,
POSTGRES_USER = "dokploy",
POSTGRES_DB = "dokploy",
POSTGRES_HOST = "dokploy-postgres",
POSTGRES_PORT = "5432",
} = process.env;
function readSecret(path: string): string {
try {
return fs.readFileSync(path, "utf8").trim();
} catch {
throw new Error(`Cannot read secret at ${path}`);
}
}
export let dbUrl: string;
if (DATABASE_URL) {
// Compatibilidad legacy / overrides
dbUrl = DATABASE_URL;
} else if (POSTGRES_PASSWORD_FILE) {
const password = readSecret(POSTGRES_PASSWORD_FILE);
dbUrl = `postgres://${POSTGRES_USER}:${encodeURIComponent(
password,
)}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`;
} else {
if (process.env.NODE_ENV !== "test") {
console.warn(`
⚠️ [DEPRECATED DATABASE CONFIG]
You are using the legacy hardcoded database credentials.
This mode WILL BE REMOVED in a future release.
Please migrate to Docker Secrets using POSTGRES_PASSWORD_FILE.
Please execute this command in your server: curl -sSL https://dokploy.com/security/0.26.6.sh | bash
`);
}
if (process.env.NODE_ENV === "production") {
dbUrl =
"postgres://dokploy:amukds4wi9001583845717ad2@dokploy-postgres:5432/dokploy";
} else {
dbUrl =
"postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy";
}
}

View File

@@ -1,5 +1,6 @@
import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { dbUrl } from "./constants";
import * as schema from "./schema";
declare global {
@@ -8,14 +9,16 @@ declare global {
export let db: PostgresJsDatabase<typeof schema>;
if (process.env.NODE_ENV === "production") {
db = drizzle(postgres(process.env.DATABASE_URL!), {
db = drizzle(postgres(dbUrl), {
schema,
});
} else {
if (!global.db)
global.db = drizzle(postgres(process.env.DATABASE_URL!), {
global.db = drizzle(postgres(dbUrl), {
schema,
});
db = global.db;
}
export { dbUrl };

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

View File

@@ -49,7 +49,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",
@@ -138,6 +138,7 @@ export const applications = pgTable("application", {
giteaBuildPath: text("giteaBuildPath").default("/"),
// Bitbucket
bitbucketRepository: text("bitbucketRepository"),
bitbucketRepositorySlug: text("bitbucketRepositorySlug"),
bitbucketOwner: text("bitbucketOwner"),
bitbucketBranch: text("bitbucketBranch"),
bitbucketBuildPath: text("bitbucketBuildPath").default("/"),
@@ -180,7 +181,7 @@ export const applications = pgTable("application", {
.notNull()
.default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"),
railpackVersion: text("railpackVersion").default("0.2.2"),
railpackVersion: text("railpackVersion").default("0.15.4"),
herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"),
@@ -289,7 +290,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(),
@@ -455,6 +461,7 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketBuildPath: true,
bitbucketOwner: true,
bitbucketRepository: true,
bitbucketRepositorySlug: true,
bitbucketId: true,
applicationId: true,
watchPaths: true,

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",
@@ -56,6 +56,7 @@ export const compose = pgTable("compose", {
gitlabPathNamespace: text("gitlabPathNamespace"),
// Bitbucket
bitbucketRepository: text("bitbucketRepository"),
bitbucketRepositorySlug: text("bitbucketRepositorySlug"),
bitbucketOwner: text("bitbucketOwner"),
bitbucketBranch: text("bitbucketBranch"),
// Gitea
@@ -146,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

@@ -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(),

View File

@@ -11,6 +11,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"),
@@ -41,6 +42,7 @@ 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
@@ -70,4 +72,5 @@ export const apiUpdateGitlab = createSchema.extend({
name: z.string().min(1),
gitlabId: z.string().min(1),
gitlabUrl: z.string().min(1),
gitlabInternalUrl: z.string().optional().nullable(),
});

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

@@ -28,7 +28,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")
@@ -99,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),
@@ -142,20 +147,18 @@ const createSchema = createInsertSchema(mariadb, {
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({

View File

@@ -35,7 +35,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")
@@ -101,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),
@@ -139,19 +144,17 @@ const createSchema = createInsertSchema(mongo, {
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({

View File

@@ -28,7 +28,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")
@@ -96,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),
@@ -139,20 +144,18 @@ const createSchema = createInsertSchema(mysql, {
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({

View File

@@ -17,8 +17,10 @@ export const notificationType = pgEnum("notificationType", [
"telegram",
"discord",
"email",
"resend",
"gotify",
"ntfy",
"pushover",
"custom",
"lark",
]);
@@ -52,6 +54,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",
}),
@@ -64,6 +69,9 @@ export const notifications = pgTable("notification", {
larkId: text("larkId").references(() => lark.larkId, {
onDelete: "cascade",
}),
pushoverId: text("pushoverId").references(() => pushover.pushoverId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
@@ -110,6 +118,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()
@@ -149,6 +167,18 @@ export const lark = pgTable("lark", {
webhookUrl: text("webhookUrl").notNull(),
});
export const pushover = pgTable("pushover", {
pushoverId: text("pushoverId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
userKey: text("userKey").notNull(),
apiToken: text("apiToken").notNull(),
priority: integer("priority").notNull().default(0),
retry: integer("retry"),
expire: integer("expire"),
});
export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, {
fields: [notifications.slackId],
@@ -166,6 +196,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],
@@ -182,6 +216,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.larkId],
references: [lark.larkId],
}),
pushover: one(pushover, {
fields: [notifications.pushoverId],
references: [pushover.pushoverId],
}),
organization: one(organization, {
fields: [notifications.organizationId],
references: [organization.id],
@@ -315,6 +353,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,
@@ -439,6 +507,69 @@ export const apiTestLarkConnection = apiCreateLark.pick({
webhookUrl: true,
});
export const apiCreatePushover = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
userKey: z.string().min(1),
apiToken: z.string().min(1),
priority: z.number().min(-2).max(2).default(0),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
})
.refine(
(data) =>
data.priority !== 2 || (data.retry != null && data.expire != null),
{
message: "Retry and expire are required for emergency priority (2)",
path: ["retry"],
},
);
export const apiUpdatePushover = z.object({
notificationId: z.string().min(1),
pushoverId: z.string().min(1),
organizationId: z.string().optional(),
userKey: z.string().min(1).optional(),
apiToken: z.string().min(1).optional(),
priority: z.number().min(-2).max(2).optional(),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
appBuildError: z.boolean().optional(),
databaseBackup: z.boolean().optional(),
volumeBackup: z.boolean().optional(),
dokployRestart: z.boolean().optional(),
name: z.string().optional(),
appDeploy: z.boolean().optional(),
dockerCleanup: z.boolean().optional(),
serverThreshold: z.boolean().optional(),
});
export const apiTestPushoverConnection = z
.object({
userKey: z.string().min(1),
apiToken: z.string().min(1),
priority: z.number().min(-2).max(2),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
})
.refine(
(data) =>
data.priority !== 2 || (data.retry != null && data.expire != null),
{
message: "Retry and expire are required for emergency priority (2)",
path: ["retry"],
},
);
export const apiSendTest = notificationsSchema
.extend({
botToken: z.string(),
@@ -451,6 +582,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(),

View File

@@ -28,7 +28,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")
@@ -97,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@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
@@ -105,7 +111,7 @@ const createSchema = createInsertSchema(postgres, {
}),
databaseName: z.string().min(1),
databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:15"),
dockerImage: z.string().default("postgres:18"),
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.string().optional(),
@@ -132,19 +138,17 @@ const createSchema = createInsertSchema(postgres, {
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({

View File

@@ -27,7 +27,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")
@@ -91,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(),
@@ -121,17 +126,15 @@ const createSchema = createInsertSchema(redis, {
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({

View File

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

View File

@@ -175,7 +175,7 @@ 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()).optional(),
})
.strict(),
);

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({
@@ -214,6 +227,6 @@ export const apiUpdateUser = createSchema.partial().extend({
.optional(),
password: z.string().optional(),
currentPassword: z.string().optional(),
name: z.string().optional(),
firstName: z.string().optional(),
lastName: z.string().optional(),
});

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

View File

@@ -131,7 +131,10 @@ export const apiAssignDomain = z
.object({
host: z.string(),
certificateType: z.enum(["letsencrypt", "none", "custom"]),
letsEncryptEmail: z.string().email().optional().nullable(),
letsEncryptEmail: z
.union([z.string().email(), z.literal("")])
.optional()
.nullable(),
https: z.boolean().optional(),
})
.required()