Merge remote-tracking branch 'origin/canary' into feat/add-github-provider-baseurl

# Conflicts:
#	apps/dokploy/drizzle/meta/0177_snapshot.json
#	apps/dokploy/drizzle/meta/_journal.json
This commit is contained in:
Narciso
2026-08-03 19:34:26 -04:00
45 changed files with 10117 additions and 167 deletions
+1
View File
@@ -38,6 +38,7 @@
"@ai-sdk/openai": "^3.0.29",
"@ai-sdk/openai-compatible": "^2.0.30",
"@better-auth/api-key": "1.6.23",
"@better-auth/passkey": "1.6.23",
"@better-auth/scim": "1.6.23",
"@better-auth/sso": "1.6.23",
"@better-auth/utils": "0.4.2",
+30
View File
@@ -223,6 +223,36 @@ export const twoFactor = pgTable("two_factor", {
lockedUntil: timestamp("locked_until"),
});
export const passkey = pgTable(
"passkey",
{
id: text("id").primaryKey(),
name: text("name"),
publicKey: text("public_key").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
credentialID: text("credential_id").notNull(),
counter: integer("counter").notNull(),
deviceType: text("device_type").notNull(),
backedUp: boolean("backed_up").notNull(),
transports: text("transports"),
createdAt: timestamp("created_at"),
aaguid: text("aaguid"),
},
(table) => [
index("passkey_userId_idx").on(table.userId),
index("passkey_credentialID_idx").on(table.credentialID),
],
);
export const passkeyRelations = relations(passkey, ({ one }) => ({
user: one(user, {
fields: [passkey.userId],
references: [user.id],
}),
}));
export const apikey = pgTable("apikey", {
id: text("id").primaryKey(),
name: text("name"),
+11 -1
View File
@@ -1,5 +1,12 @@
import { relations } from "drizzle-orm";
import { boolean, jsonb, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import {
boolean,
integer,
jsonb,
pgEnum,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -19,6 +26,7 @@ export const network = pgTable("network", {
attachable: boolean("attachable").notNull().default(false),
enableIPv4: boolean("enableIPv4").notNull().default(true),
enableIPv6: boolean("enableIPv6").notNull().default(false),
mtu: integer("mtu"),
ipam: jsonb("ipam")
.$type<{
driver?: string;
@@ -55,6 +63,7 @@ const createSchema = createInsertSchema(network, {
attachable: z.boolean().optional(),
enableIPv4: z.boolean().optional(),
enableIPv6: z.boolean().optional(),
mtu: z.number().int().min(68).max(65535).optional().nullable(),
ipam: z
.object({
driver: z.string().optional(),
@@ -109,6 +118,7 @@ export const apiCreateNetwork = createSchema
attachable: true,
enableIPv4: true,
enableIPv6: true,
mtu: true,
ipam: true,
serverId: true,
})
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Application } from "@dokploy/server/services/application";
import type { Environment } from "@dokploy/server/services/environment";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Project } from "@dokploy/server/services/project";
@@ -27,7 +28,7 @@ export const rollbacks = pgTable("rollback", {
.$defaultFn(() => new Date().toISOString()),
fullContext: jsonb("fullContext").$type<
Application & {
environment: {
environment: Environment & {
project: Project;
};
mounts: Mount[];
+2
View File
@@ -1,4 +1,5 @@
import { apiKey } from "@better-auth/api-key";
import { passkey } from "@better-auth/passkey";
import { scim } from "@better-auth/scim";
import { sso } from "@better-auth/sso";
import { betterAuth } from "better-auth";
@@ -33,6 +34,7 @@ export const auth = betterAuth({
apiKey({ enableMetadata: true, references: "user" }),
sso(),
twoFactor(),
passkey(),
organization({
ac,
roles: { owner: ownerRole, admin: adminRole, member: memberRole },
+2
View File
@@ -1,5 +1,6 @@
import type { IncomingMessage } from "node:http";
import { apiKey } from "@better-auth/api-key";
import { passkey } from "@better-auth/passkey";
import { scim } from "@better-auth/scim";
import { sso } from "@better-auth/sso";
import * as bcrypt from "bcrypt";
@@ -435,6 +436,7 @@ const createBetterAuth = () =>
},
}),
twoFactor(),
passkey(),
organization({
ac,
roles: {
+10
View File
@@ -24,6 +24,7 @@ type DockerNetworkInfo = {
Ingress?: boolean;
EnableIPv4?: boolean;
EnableIPv6?: boolean;
Options?: Record<string, string> | null;
IPAM?: {
Driver?: string;
Config?: Array<{
@@ -34,6 +35,11 @@ type DockerNetworkInfo = {
};
};
const parseMtu = (value: string | undefined) => {
const mtu = Number.parseInt(value ?? "", 10);
return Number.isNaN(mtu) ? null : mtu;
};
const isImportableDockerNetwork = (dockerNetwork: DockerNetworkInfo) =>
!RESERVED_NETWORKS.includes(dockerNetwork.Name) &&
!dockerNetwork.Ingress &&
@@ -51,6 +57,7 @@ const mapDockerNetworkToRow = (
// Older daemons don't report EnableIPv4; IPv4 is always on there
enableIPv4: dockerNetwork.EnableIPv4 ?? true,
enableIPv6: dockerNetwork.EnableIPv6 ?? false,
mtu: parseMtu(dockerNetwork.Options?.["com.docker.network.driver.mtu"]),
ipam: {
driver: dockerNetwork.IPAM?.Driver,
config: (dockerNetwork.IPAM?.Config ?? []).map((c) => ({
@@ -255,6 +262,9 @@ const createDockerNetworkFromRow = async (row: typeof network.$inferSelect) => {
// the daemon (API >= 1.47); the body is sent as-is
EnableIPv4: row.enableIPv4,
EnableIPv6: row.enableIPv6,
Options: row.mtu
? { "com.docker.network.driver.mtu": String(row.mtu) }
: undefined,
IPAM: {
Driver: ipam.driver || "default",
Config: ipamConfig.length > 0 ? ipamConfig : undefined,
@@ -17,7 +17,7 @@ import { manageDomain } from "../utils/traefik/domain";
import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github";
import { findGithubById, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -142,7 +142,17 @@ export const createPreviewDeployment = async (
org?.ownerId || "",
);
const octokit = authGithub(application?.github as Github);
if (!application.githubId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Github Account not configured correctly",
});
}
// `findApplicationById` redacts `githubPrivateKey` from the `github`
// relation, so the provider must be refetched to authenticate.
const githubProvider = await findGithubById(application.githubId);
const octokit = authGithub(githubProvider);
const runningComment = getIssueComment(
application.name,
+3 -1
View File
@@ -19,6 +19,7 @@ import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import { type Application, findApplicationById } from "./application";
import { findDeploymentById } from "./deployment";
import type { Environment } from "./environment";
import type { Mount } from "./mount";
import { resolveServiceNetworks } from "./network";
import type { Port } from "./port";
@@ -202,7 +203,7 @@ const rollbackApplication = async (
image: string,
serverId?: string | null,
fullContext?: Application & {
environment: {
environment: Environment & {
project: Project;
};
mounts: Mount[];
@@ -267,6 +268,7 @@ const rollbackApplication = async (
const envVariables = prepareEnvironmentVariables(
env,
fullContext.environment.project.env,
fullContext.environment.env,
);
let rollbackImage = image;
+17 -1
View File
@@ -4,11 +4,12 @@ import {
apikey,
invitation,
member,
passkey,
user,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { and, eq } from "drizzle-orm";
import { and, desc, eq } from "drizzle-orm";
import { auth } from "../lib/auth";
export type User = typeof user.$inferSelect;
@@ -396,6 +397,21 @@ export const findMemberById = async (
return result;
};
export const findPasskeysByUserId = async (userId: string) => {
return db.query.passkey.findMany({
where: eq(passkey.userId, userId),
columns: {
id: true,
name: true,
deviceType: true,
backedUp: true,
createdAt: true,
aaguid: true,
},
orderBy: [desc(passkey.createdAt)],
});
};
export const createOrganizationUserWithCredentials = async ({
organizationId,
email,
@@ -53,14 +53,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
"build",
"--builder",
builderName,
...(cacheKey
? [
"--build-arg",
`secrets-hash=${secretsHash}`,
"--build-arg",
`cache-key=${cacheKey}`,
]
: []),
"--build-arg",
`secrets-hash=${secretsHash}`,
...(cacheKey ? ["--build-arg", `cache-key=${cacheKey}`] : []),
"--build-arg",
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f",
+78 -64
View File
@@ -55,25 +55,46 @@ export const runCommand = async (scheduleId: string) => {
description: "Schedule",
});
if (scheduleType === "application" || scheduleType === "compose") {
let containerId = "";
let serverId = "";
if (scheduleType === "application" && application) {
const container = await getServiceContainer(
application.appName,
application.serverId,
);
containerId = container?.Id || "";
serverId = application.serverId || "";
}
if (scheduleType === "compose" && compose) {
const container = await getComposeContainer(compose, serviceName || "");
containerId = container?.Id || "";
serverId = compose.serverId || "";
}
try {
if (scheduleType === "application" || scheduleType === "compose") {
let containerId = "";
let serverId = "";
if (scheduleType === "application" && application) {
const container = await getServiceContainer(
application.appName,
application.serverId,
);
containerId = container?.Id || "";
serverId = application.serverId || "";
}
if (scheduleType === "compose" && compose) {
const container = await getComposeContainer(compose, serviceName || "");
containerId = container?.Id || "";
serverId = compose.serverId || "";
}
if (serverId) {
try {
if (!containerId) {
const target =
scheduleType === "compose"
? `service '${serviceName}' of compose '${compose?.name}'`
: `application '${application?.appName}'`;
const message = `Container not found for ${target}, make sure the service is running`;
if (serverId) {
await execAsyncRemote(
serverId,
`echo ${quote([`❌ ${message}`])} >> ${quote([deployment.logPath])}`,
);
} else {
const writeStream = createWriteStream(deployment.logPath, {
flags: "a",
});
writeStream.write(`❌ ${message}\n`);
writeStream.end();
}
throw new Error(message);
}
if (serverId) {
await execAsyncRemote(
serverId,
`
@@ -86,47 +107,44 @@ export const runCommand = async (scheduleId: string) => {
echo "✅ Command executed successfully" >> ${quote([deployment.logPath])};
`,
);
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
} else {
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
} else {
const writeStream = createWriteStream(deployment.logPath, {
flags: "a",
});
try {
if (IS_CLOUD) {
try {
if (IS_CLOUD) {
writeStream.write(
"This feature is not available in the cloud version.",
);
writeStream.end();
return { ...deployment, status: "running" as const };
}
writeStream.write(
"This feature is not available in the cloud version.",
`docker exec ${containerId} ${shellType} -c ${command}\n`,
);
await spawnAsync(
"docker",
["exec", containerId, shellType, "-c", command],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
writeStream.write("✅ Command executed successfully\n");
writeStream.end();
} catch (error) {
writeStream.write("❌ Command failed\n");
writeStream.write(
error instanceof Error ? error.message : "Unknown error",
);
writeStream.end();
return;
throw error;
}
writeStream.write(
`docker exec ${containerId} ${shellType} -c ${command}\n`,
);
await spawnAsync(
"docker",
["exec", containerId, shellType, "-c", command],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
writeStream.write("✅ Command executed successfully\n");
} catch (error) {
writeStream.write("❌ Command failed\n");
writeStream.write(
error instanceof Error ? error.message : "Unknown error",
);
writeStream.end();
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
}
} else if (scheduleType === "dokploy-server") {
try {
} else if (scheduleType === "dokploy-server") {
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
const { SCHEDULES_PATH } = paths();
const fullPath = path.join(SCHEDULES_PATH, appName || "");
@@ -151,18 +169,13 @@ export const runCommand = async (scheduleId: string) => {
cwd: fullPath,
},
);
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
} else if (scheduleType === "server") {
try {
} else if (scheduleType === "server") {
const { SCHEDULES_PATH } = paths(true);
const fullPath = path.join(SCHEDULES_PATH, appName || "");
const command = `
set -e
echo "Running script" >> ${deployment.logPath};
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
echo "❌ Command failed" >> ${deployment.logPath};
exit 1;
}
@@ -177,10 +190,11 @@ export const runCommand = async (scheduleId: string) => {
});
}
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
await updateDeploymentStatus(deployment.deploymentId, "done");
return { ...deployment, status: "done" as const };
} catch {
await updateDeploymentStatus(deployment.deploymentId, "error");
return { ...deployment, status: "error" as const };
}
await updateDeploymentStatus(deployment.deploymentId, "done");
};
+1 -1
View File
@@ -17,7 +17,7 @@
}
},
"include": ["next-env.d.ts", "./src/**/*"],
"exclude": ["**/dist", "tsup.ts"],
"exclude": ["**/dist", "tsup.ts", "src/lib/auth-cli.ts"],
"tsc-alias": {
"resolveFullPaths": true,
"verbose": false