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

@@ -4,6 +4,10 @@ import Docker from "dockerode";
export const IS_CLOUD = process.env.IS_CLOUD === "true";
export const docker = new Docker();
export const BETTER_AUTH_SECRET =
process.env.BETTER_AUTH_SECRET ||
"RXu/xoLHaA1Xgs+R8a0LjVjCVOEnWISQWxw7nXxlvKo=";
export const paths = (isServer = false) => {
const BASE_PATH =
isServer || process.env.NODE_ENV === "production"

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

View File

@@ -31,6 +31,7 @@ export * from "./services/port";
export * from "./services/postgres";
export * from "./services/preview-deployment";
export * from "./services/project";
export * from "./services/proprietary/sso";
export * from "./services/redirect";
export * from "./services/redis";
export * from "./services/registry";
@@ -79,6 +80,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";

View File

@@ -1,14 +1,15 @@
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, getUserByToken } from "../services/admin";
import {
getWebServerSettings,
updateWebServerSettings,
@@ -22,6 +23,13 @@ const { handler, api } = betterAuth({
provider: "pg",
schema: schema,
}),
disabledPaths: [
"/sso/register",
"/organization/create",
"/organization/update",
"/organization/delete",
],
secret: BETTER_AUTH_SECRET,
appName: "Dokploy",
socialProviders: {
github: {
@@ -36,24 +44,24 @@ 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"]
: []),
...trustedOrigins,
];
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
@@ -106,6 +114,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 +130,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 +186,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 +270,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) {
@@ -273,6 +320,7 @@ const { handler, api } = betterAuth({
export const auth = {
handler,
createApiKey: api.createApiKey,
registerSSOProvider: api.registerSSOProvider,
};
export const validateRequest = async (request: IncomingMessage) => {
@@ -352,6 +400,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,
},
};
@@ -390,10 +440,15 @@ export const validateRequest = async (request: IncomingMessage) => {
),
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;
if (member) {
session.user.ownerId = member.organization.ownerId;
} else {

View File

@@ -116,3 +116,22 @@ 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));
};

View File

@@ -18,7 +18,7 @@ export type Mariadb = typeof mariadb.$inferSelect;
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
const appName = buildAppName("mariadb", input.appName);
const valid = await validUniqueServerAppName(input.appName);
const valid = await validUniqueServerAppName(appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",

View File

@@ -0,0 +1,35 @@
import { db } from "@dokploy/server/db";
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(/\/+$/, "");
};

View File

@@ -103,7 +103,7 @@ export const getProviderHeaders = (
// Mistral
if (apiUrl.includes("mistral")) {
return {
Authorization: apiKey,
Authorization: `Bearer ${apiKey}`,
};
}

View 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.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;
}
};