Merge branch 'canary' into feat/migration-templates

This commit is contained in:
Mauricio Siu
2025-03-09 12:36:14 -06:00
452 changed files with 78354 additions and 6009 deletions

View File

@@ -1,117 +0,0 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { findAdminByAuthId } from "@dokploy/server/services/admin";
import { findUserByAuthId } from "@dokploy/server/services/user";
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { TimeSpan } from "lucia";
import { Lucia } from "lucia/dist/core.js";
import type { Session, User } from "lucia/dist/core.js";
import { db } from "../db";
import { type DatabaseUser, auth, sessionTable } from "../db/schema";
export const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, auth);
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: false,
},
},
sessionExpiresIn: new TimeSpan(1, "d"),
getUserAttributes: (attributes) => {
return {
email: attributes.email,
rol: attributes.rol,
secret: attributes.secret !== null,
adminId: attributes.adminId,
};
},
});
declare module "lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: Omit<DatabaseUser, "id"> & {
authId: string;
adminId: string;
};
}
}
export type ReturnValidateToken = Promise<{
user: (User & { authId: string; adminId: string }) | null;
session: Session | null;
}>;
export async function validateRequest(
req: IncomingMessage,
res: ServerResponse,
): ReturnValidateToken {
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
if (!sessionId) {
return {
user: null,
session: null,
};
}
const result = await lucia.validateSession(sessionId);
if (result?.session?.fresh) {
res.appendHeader(
"Set-Cookie",
lucia.createSessionCookie(result.session.id).serialize(),
);
}
if (!result.session) {
res.appendHeader(
"Set-Cookie",
lucia.createBlankSessionCookie().serialize(),
);
}
if (result.user) {
try {
if (result.user?.rol === "admin") {
const admin = await findAdminByAuthId(result.user.id);
result.user.adminId = admin.adminId;
} else if (result.user?.rol === "user") {
const userResult = await findUserByAuthId(result.user.id);
result.user.adminId = userResult.adminId;
}
} catch (error) {
return {
user: null,
session: null,
};
}
}
return {
session: result.session,
...((result.user && {
user: {
authId: result.user.id,
email: result.user.email,
rol: result.user.rol,
id: result.user.id,
secret: result.user.secret,
adminId: result.user.adminId,
},
}) || {
user: null,
}),
};
}
export async function validateWebSocketRequest(
req: IncomingMessage,
): Promise<{ user: User; session: Session } | { user: null; session: null }> {
const sessionId = lucia.readSessionCookie(req.headers.cookie ?? "");
if (!sessionId) {
return {
user: null,
session: null,
};
}
const result = await lucia.validateSession(sessionId);
return result;
}

View File

@@ -1,99 +0,0 @@
import type { IncomingMessage } from "node:http";
import { TimeSpan } from "lucia";
import { Lucia } from "lucia/dist/core.js";
import { findAdminByAuthId } from "../services/admin";
import { findUserByAuthId } from "../services/user";
import { type ReturnValidateToken, adapter } from "./auth";
export const luciaToken = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: false,
},
},
sessionExpiresIn: new TimeSpan(365, "d"),
getUserAttributes: (attributes) => {
return {
email: attributes.email,
rol: attributes.rol,
secret: attributes.secret !== null,
};
},
});
export const validateBearerToken = async (
req: IncomingMessage,
): ReturnValidateToken => {
const authorizationHeader = req.headers.authorization;
const sessionId = luciaToken.readBearerToken(authorizationHeader ?? "");
if (!sessionId) {
return {
user: null,
session: null,
};
}
const result = await luciaToken.validateSession(sessionId);
if (result.user) {
if (result.user?.rol === "admin") {
const admin = await findAdminByAuthId(result.user.id);
result.user.adminId = admin.adminId;
} else if (result.user?.rol === "user") {
const userResult = await findUserByAuthId(result.user.id);
result.user.adminId = userResult.adminId;
}
}
return {
session: result.session,
...((result.user && {
user: {
adminId: result.user.adminId,
authId: result.user.id,
email: result.user.email,
rol: result.user.rol,
id: result.user.id,
secret: result.user.secret,
},
}) || {
user: null,
}),
};
};
export const validateBearerTokenAPI = async (
authorizationHeader: string,
): ReturnValidateToken => {
const sessionId = luciaToken.readBearerToken(authorizationHeader ?? "");
if (!sessionId) {
return {
user: null,
session: null,
};
}
const result = await luciaToken.validateSession(sessionId);
if (result.user) {
if (result.user?.rol === "admin") {
const admin = await findAdminByAuthId(result.user.id);
result.user.adminId = admin.adminId;
} else if (result.user?.rol === "user") {
const userResult = await findUserByAuthId(result.user.id);
result.user.adminId = userResult.adminId;
}
}
return {
session: result.session,
...((result.user && {
user: {
adminId: result.user.adminId,
authId: result.user.id,
email: result.user.email,
rol: result.user.rol,
id: result.user.id,
secret: result.user.secret,
},
}) || {
user: null,
}),
};
};

View File

@@ -0,0 +1,194 @@
import { relations, sql } from "drizzle-orm";
import {
boolean,
integer,
pgTable,
text,
timestamp,
} from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
import { projects } from "./project";
import { server } from "./server";
import { users_temp } from "./user";
export const account = pgTable("account", {
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
accountId: text("account_id")
.notNull()
.$defaultFn(() => nanoid()),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => users_temp.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"),
is2FAEnabled: boolean("is2FAEnabled").notNull().default(false),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
resetPasswordToken: text("resetPasswordToken"),
resetPasswordExpiresAt: text("resetPasswordExpiresAt"),
confirmationToken: text("confirmationToken"),
confirmationExpiresAt: text("confirmationExpiresAt"),
});
export const accountRelations = relations(account, ({ one }) => ({
user: one(users_temp, {
fields: [account.userId],
references: [users_temp.id],
}),
}));
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"),
updatedAt: timestamp("updated_at"),
});
export const organization = pgTable("organization", {
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
slug: text("slug").unique(),
logo: text("logo"),
createdAt: timestamp("created_at").notNull(),
metadata: text("metadata"),
ownerId: text("owner_id")
.notNull()
.references(() => users_temp.id, { onDelete: "cascade" }),
});
export const organizationRelations = relations(
organization,
({ one, many }) => ({
owner: one(users_temp, {
fields: [organization.ownerId],
references: [users_temp.id],
}),
servers: many(server),
projects: many(projects),
members: many(member),
}),
);
export const member = pgTable("member", {
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
organizationId: text("organization_id")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => users_temp.id, { onDelete: "cascade" }),
role: text("role").notNull().$type<"owner" | "member" | "admin">(),
createdAt: timestamp("created_at").notNull(),
teamId: text("team_id"),
// Permissions
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
canCreateServices: boolean("canCreateServices").notNull().default(false),
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
canAccessToAPI: boolean("canAccessToAPI").notNull().default(false),
canAccessToGitProviders: boolean("canAccessToGitProviders")
.notNull()
.default(false),
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull()
.default(false),
accessedProjects: text("accesedProjects")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
accessedServices: text("accesedServices")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
});
export const memberRelations = relations(member, ({ one }) => ({
organization: one(organization, {
fields: [member.organizationId],
references: [organization.id],
}),
user: one(users_temp, {
fields: [member.userId],
references: [users_temp.id],
}),
}));
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").$type<"owner" | "member" | "admin">(),
status: text("status").notNull(),
expiresAt: timestamp("expires_at").notNull(),
inviterId: text("inviter_id")
.notNull()
.references(() => users_temp.id, { onDelete: "cascade" }),
teamId: text("team_id"),
});
export const invitationRelations = relations(invitation, ({ one }) => ({
organization: one(organization, {
fields: [invitation.organizationId],
references: [organization.id],
}),
}));
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(() => users_temp.id, { onDelete: "cascade" }),
});
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(() => users_temp.id, { onDelete: "cascade" }),
refillInterval: integer("refill_interval"),
refillAmount: integer("refill_amount"),
lastRefillAt: timestamp("last_refill_at"),
enabled: boolean("enabled"),
rateLimitEnabled: boolean("rate_limit_enabled"),
rateLimitTimeWindow: integer("rate_limit_time_window"),
rateLimitMax: integer("rate_limit_max"),
requestCount: integer("request_count"),
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"),
});
export const apikeyRelations = relations(apikey, ({ one }) => ({
user: one(users_temp, {
fields: [apikey.userId],
references: [users_temp.id],
}),
}));

View File

@@ -1,210 +0,0 @@
import { relations } from "drizzle-orm";
import {
boolean,
integer,
json,
jsonb,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { auth } from "./auth";
import { certificates } from "./certificate";
import { registry } from "./registry";
import { certificateType } from "./shared";
import { sshKeys } from "./ssh-key";
import { users } from "./user";
export const admins = pgTable("admin", {
adminId: text("adminId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
enableLogRotation: boolean("enableLogRotation").notNull().default(false),
authId: text("authId")
.notNull()
.references(() => auth.id, { onDelete: "cascade" }),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0),
// Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
});
export const adminsRelations = relations(admins, ({ one, many }) => ({
auth: one(auth, {
fields: [admins.authId],
references: [auth.id],
}),
users: many(users),
registry: many(registry),
sshKeys: many(sshKeys),
certificates: many(certificates),
}));
const createSchema = createInsertSchema(admins, {
adminId: z.string(),
enableDockerCleanup: z.boolean().optional(),
sshPrivateKey: z.string().optional(),
certificateType: z.enum(["letsencrypt", "none"]).default("none"),
serverIp: z.string().optional(),
letsEncryptEmail: z.string().optional(),
});
export const apiUpdateAdmin = createSchema.partial();
export const apiSaveSSHKey = createSchema
.pick({
sshPrivateKey: true,
})
.required();
export const apiAssignDomain = createSchema
.pick({
host: true,
certificateType: true,
letsEncryptEmail: true,
})
.required()
.partial({
letsEncryptEmail: true,
});
export const apiUpdateDockerCleanup = createSchema
.pick({
enableDockerCleanup: true,
})
.required()
.extend({
serverId: z.string().optional(),
});
export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1),
});
export const apiModifyTraefikConfig = z.object({
path: z.string().min(1),
traefikConfig: z.string().min(1),
serverId: z.string().optional(),
});
export const apiReadTraefikConfig = z.object({
path: z.string().min(1),
serverId: z.string().optional(),
});
export const apiEnableDashboard = z.object({
enableDashboard: z.boolean().optional(),
serverId: z.string().optional(),
});
export const apiServerSchema = z
.object({
serverId: z.string().optional(),
})
.optional();
export const apiReadStatsLogs = z.object({
page: z
.object({
pageIndex: z.number(),
pageSize: z.number(),
})
.optional(),
status: z.string().array().optional(),
search: z.string().optional(),
sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});

View File

@@ -0,0 +1,82 @@
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 { organization } from "./account";
export const ai = pgTable("ai", {
aiId: text("aiId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
apiUrl: text("apiUrl").notNull(),
apiKey: text("apiKey").notNull(),
model: text("model").notNull(),
isEnabled: boolean("isEnabled").notNull().default(true),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }), // Admin ID who created the AI settings
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
});
export const aiRelations = relations(ai, ({ one }) => ({
organization: one(organization, {
fields: [ai.organizationId],
references: [organization.id],
}),
}));
const createSchema = createInsertSchema(ai, {
name: z.string().min(1, { message: "Name is required" }),
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
apiKey: z.string().min(1, { message: "API Key is required" }),
model: z.string().min(1, { message: "Model is required" }),
isEnabled: z.boolean().optional(),
});
export const apiCreateAi = createSchema
.pick({
name: true,
apiUrl: true,
apiKey: true,
model: true,
isEnabled: true,
})
.required();
export const apiUpdateAi = createSchema
.partial()
.extend({
aiId: z.string().min(1),
})
.omit({ organizationId: true });
export const deploySuggestionSchema = z.object({
projectId: z.string().min(1),
id: z.string().min(1),
dockerCompose: z.string().min(1),
envVariables: z.string(),
serverId: z.string().optional(),
name: z.string().min(1),
description: z.string(),
domains: z
.array(
z.object({
host: z.string().min(1),
port: z.number().min(1),
serviceName: z.string().min(1),
}),
)
.optional(),
configFiles: z
.array(
z.object({
filePath: z.string().min(1),
content: z.string().min(1),
}),
)
.optional(),
});

View File

@@ -42,9 +42,9 @@ export const buildType = pgEnum("buildType", [
"paketo_buildpacks",
"nixpacks",
"static",
"railpack",
]);
// TODO: refactor this types
export interface HealthCheckSwarm {
Test?: string[] | undefined;
Interval?: number | undefined;
@@ -116,6 +116,7 @@ export const applications = pgTable("application", {
description: text("description"),
env: text("env"),
previewEnv: text("previewEnv"),
watchPaths: text("watchPaths").array(),
previewBuildArgs: text("previewBuildArgs"),
previewWildcard: text("previewWildcard"),
previewPort: integer("previewPort").default(3000),
@@ -124,6 +125,7 @@ export const applications = pgTable("application", {
previewCertificateType: certificateType("certificateType")
.notNull()
.default("none"),
previewCustomCertResolver: text("previewCustomCertResolver"),
previewLimit: integer("previewLimit").default(3),
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
false,
@@ -384,6 +386,7 @@ const createSchema = createInsertSchema(applications, {
"paketo_buildpacks",
"nixpacks",
"static",
"railpack",
]),
herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(),
@@ -403,7 +406,8 @@ const createSchema = createInsertSchema(applications, {
previewLimit: z.number().optional(),
previewHttps: z.boolean().optional(),
previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none"]).optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
watchPaths: z.array(z.string()).optional(),
});
export const apiCreateApplication = createSchema.pick({
@@ -447,6 +451,7 @@ export const apiSaveGithubProvider = createSchema
owner: true,
buildPath: true,
githubId: true,
watchPaths: true,
})
.required();
@@ -460,6 +465,7 @@ export const apiSaveGitlabProvider = createSchema
gitlabId: true,
gitlabProjectId: true,
gitlabPathNamespace: true,
watchPaths: true,
})
.required();
@@ -471,6 +477,7 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketRepository: true,
bitbucketId: true,
applicationId: true,
watchPaths: true,
})
.required();
@@ -490,6 +497,7 @@ export const apiSaveGitProvider = createSchema
applicationId: true,
customGitBuildPath: true,
customGitUrl: true,
watchPaths: true,
})
.required()
.merge(

View File

@@ -1,130 +0,0 @@
import { getRandomValues } from "node:crypto";
import { relations } from "drizzle-orm";
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { admins } from "./admin";
import { users } from "./user";
const randomImages = [
"/avatars/avatar-1.png",
"/avatars/avatar-2.png",
"/avatars/avatar-3.png",
"/avatars/avatar-4.png",
"/avatars/avatar-5.png",
"/avatars/avatar-6.png",
"/avatars/avatar-7.png",
"/avatars/avatar-8.png",
"/avatars/avatar-9.png",
"/avatars/avatar-10.png",
"/avatars/avatar-11.png",
"/avatars/avatar-12.png",
];
const generateRandomImage = () => {
return (
randomImages[
// @ts-ignore
getRandomValues(new Uint32Array(1))[0] % randomImages.length
] || "/avatars/avatar-1.png"
);
};
export type DatabaseUser = typeof auth.$inferSelect;
export const roles = pgEnum("Roles", ["admin", "user"]);
export const auth = pgTable("auth", {
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
email: text("email").notNull().unique(),
password: text("password").notNull(),
rol: roles("rol").notNull(),
image: text("image").$defaultFn(() => generateRandomImage()),
secret: text("secret"),
token: text("token"),
is2FAEnabled: boolean("is2FAEnabled").notNull().default(false),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
resetPasswordToken: text("resetPasswordToken"),
resetPasswordExpiresAt: text("resetPasswordExpiresAt"),
confirmationToken: text("confirmationToken"),
confirmationExpiresAt: text("confirmationExpiresAt"),
});
export const authRelations = relations(auth, ({ many }) => ({
admins: many(admins),
users: many(users),
}));
const createSchema = createInsertSchema(auth, {
email: z.string().email(),
password: z.string().min(8),
rol: z.enum(["admin", "user"]),
image: z.string().optional(),
});
export const apiCreateAdmin = createSchema.pick({
email: true,
password: true,
});
export const apiCreateUser = createSchema
.pick({
password: true,
id: true,
token: true,
})
.required()
.extend({
token: z.string().min(1),
});
export const apiLogin = createSchema
.pick({
email: true,
password: true,
})
.required();
export const apiUpdateAuth = createSchema.partial().extend({
email: z.string().nullable(),
password: z.string().nullable(),
image: z.string().optional(),
currentPassword: z.string().nullable(),
});
export const apiUpdateAuthByAdmin = createSchema.partial().extend({
email: z.string().nullable(),
password: z.string().nullable(),
image: z.string().optional(),
id: z.string().min(1),
});
export const apiFindOneAuth = createSchema
.pick({
id: true,
})
.required();
export const apiVerify2FA = createSchema
.extend({
pin: z.string().min(6),
secret: z.string().min(1),
})
.pick({
pin: true,
secret: true,
})
.required();
export const apiVerifyLogin2FA = createSchema
.extend({
pin: z.string().min(6),
})
.pick({
pin: true,
id: true,
})
.required();

View File

@@ -2,6 +2,7 @@ import { relations } from "drizzle-orm";
import {
type AnyPgColumn,
boolean,
integer,
pgEnum,
pgTable,
text,
@@ -36,6 +37,8 @@ export const backups = pgTable("backup", {
.notNull()
.references(() => destinations.destinationId, { onDelete: "cascade" }),
keepLatestCount: integer("keepLatestCount"),
databaseType: databaseType("databaseType").notNull(),
postgresId: text("postgresId").references(
(): AnyPgColumn => postgres.postgresId,
@@ -87,6 +90,7 @@ const createSchema = createInsertSchema(backups, {
prefix: z.string().min(1),
database: z.string().min(1),
schedule: z.string(),
keepLatestCount: z.number().optional(),
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo"]),
postgresId: z.string().optional(),
mariadbId: z.string().optional(),
@@ -99,6 +103,7 @@ export const apiCreateBackup = createSchema.pick({
enabled: true,
prefix: true,
destinationId: true,
keepLatestCount: true,
database: true,
mariadbId: true,
mysqlId: true,
@@ -127,5 +132,6 @@ export const apiUpdateBackup = createSchema
backupId: true,
destinationId: true,
database: true,
keepLatestCount: true,
})
.required();

View File

@@ -61,5 +61,5 @@ export const apiUpdateBitbucket = createSchema.extend({
name: z.string().min(1),
bitbucketUsername: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(),
adminId: z.string().optional(),
organizationId: z.string().optional(),
});

View File

@@ -3,7 +3,7 @@ import { boolean, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { admins } from "./admin";
import { organization } from "./account";
import { server } from "./server";
import { generateAppName } from "./utils";
@@ -20,27 +20,24 @@ export const certificates = pgTable("certificate", {
.$defaultFn(() => generateAppName("certificate"))
.unique(),
autoRenew: boolean("autoRenew"),
adminId: text("adminId").references(() => admins.adminId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const certificatesRelations = relations(
certificates,
({ one, many }) => ({
server: one(server, {
fields: [certificates.serverId],
references: [server.serverId],
}),
admin: one(admins, {
fields: [certificates.adminId],
references: [admins.adminId],
}),
export const certificatesRelations = relations(certificates, ({ one }) => ({
server: one(server, {
fields: [certificates.serverId],
references: [server.serverId],
}),
);
organization: one(organization, {
fields: [certificates.organizationId],
references: [organization.id],
}),
}));
export const apiCreateCertificate = createInsertSchema(certificates, {
name: z.string().min(1),

View File

@@ -77,7 +77,7 @@ export const compose = pgTable("compose", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
watchPaths: text("watchPaths").array(),
githubId: text("githubId").references(() => github.githubId, {
onDelete: "set null",
}),
@@ -132,6 +132,7 @@ const createSchema = createInsertSchema(compose, {
command: z.string().optional(),
composePath: z.string().min(1),
composeType: z.enum(["docker-compose", "stack"]).optional(),
watchPaths: z.array(z.string()).optional(),
});
export const apiCreateCompose = createSchema.pick({

View File

@@ -0,0 +1,7 @@
import { pgGenerate } from "drizzle-dbml-generator"; // Using Postgres for this example
import * as schema from "./index";
const out = "./schema.dbml";
const relational = true;
pgGenerate({ schema, out, relational });

View File

@@ -1,4 +1,4 @@
import { is, relations } from "drizzle-orm";
import { relations } from "drizzle-orm";
import {
type AnyPgColumn,
boolean,

View File

@@ -1,9 +1,9 @@
import { relations } from "drizzle-orm";
import { pgTable, text } from "drizzle-orm/pg-core";
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { admins } from "./admin";
import { organization } from "./account";
import { backups } from "./backups";
export const destinations = pgTable("destination", {
@@ -17,20 +17,20 @@ export const destinations = pgTable("destination", {
secretAccessKey: text("secretAccessKey").notNull(),
bucket: text("bucket").notNull(),
region: text("region").notNull(),
// maybe it can be null
endpoint: text("endpoint").notNull(),
adminId: text("adminId")
organizationId: text("organizationId")
.notNull()
.references(() => admins.adminId, { onDelete: "cascade" }),
.references(() => organization.id, { onDelete: "cascade" }),
createdAt: timestamp("createdAt").notNull().defaultNow(),
});
export const destinationsRelations = relations(
destinations,
({ many, one }) => ({
backups: many(backups),
admin: one(admins, {
fields: [destinations.adminId],
references: [admins.adminId],
organization: one(organization, {
fields: [destinations.organizationId],
references: [organization.id],
}),
}),
);

View File

@@ -41,6 +41,7 @@ export const domains = pgTable("domain", {
composeId: text("composeId").references(() => compose.composeId, {
onDelete: "cascade",
}),
customCertResolver: text("customCertResolver"),
applicationId: text("applicationId").references(
() => applications.applicationId,
{ onDelete: "cascade" },
@@ -76,6 +77,7 @@ export const apiCreateDomain = createSchema.pick({
https: true,
applicationId: true,
certificateType: true,
customCertResolver: true,
composeId: true,
serviceName: true,
domainType: true,
@@ -107,6 +109,7 @@ export const apiUpdateDomain = createSchema
port: true,
https: true,
certificateType: true,
customCertResolver: true,
serviceName: true,
domainType: true,
})

View File

@@ -3,7 +3,7 @@ import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { admins } from "./admin";
import { organization } from "./account";
import { bitbucket } from "./bitbucket";
import { github } from "./github";
import { gitlab } from "./gitlab";
@@ -24,12 +24,12 @@ export const gitProvider = pgTable("git_provider", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
adminId: text("adminId").references(() => admins.adminId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
});
export const gitProviderRelations = relations(gitProvider, ({ one, many }) => ({
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
github: one(github, {
fields: [gitProvider.gitProviderId],
references: [github.gitProviderId],
@@ -42,9 +42,9 @@ export const gitProviderRelations = relations(gitProvider, ({ one, many }) => ({
fields: [gitProvider.gitProviderId],
references: [bitbucket.gitProviderId],
}),
admin: one(admins, {
fields: [gitProvider.adminId],
references: [admins.adminId],
organization: one(organization, {
fields: [gitProvider.organizationId],
references: [organization.id],
}),
}));

View File

@@ -1,8 +1,6 @@
export * from "./application";
export * from "./postgres";
export * from "./user";
export * from "./admin";
export * from "./auth";
export * from "./project";
export * from "./domain";
export * from "./mariadb";
@@ -30,3 +28,5 @@ export * from "./gitlab";
export * from "./server";
export * from "./utils";
export * from "./preview-deployments";
export * from "./ai";
export * from "./account";

View File

@@ -146,3 +146,9 @@ export const apiUpdateMariaDB = createSchema
mariadbId: z.string().min(1),
})
.omit({ serverId: true });
export const apiRebuildMariadb = createSchema
.pick({
mariadbId: true,
})
.required();

View File

@@ -141,3 +141,9 @@ export const apiResetMongo = createSchema
appName: true,
})
.required();
export const apiRebuildMongo = createSchema
.pick({
mongoId: true,
})
.required();

View File

@@ -144,3 +144,9 @@ export const apiUpdateMySql = createSchema
mysqlId: z.string().min(1),
})
.omit({ serverId: true });
export const apiRebuildMysql = createSchema
.pick({
mysqlId: true,
})
.required();

View File

@@ -3,7 +3,7 @@ import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { admins } from "./admin";
import { organization } from "./account";
export const notificationType = pgEnum("notificationType", [
"slack",
@@ -44,9 +44,9 @@ export const notifications = pgTable("notification", {
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade",
}),
adminId: text("adminId").references(() => admins.adminId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
});
export const slack = pgTable("slack", {
@@ -65,6 +65,7 @@ export const telegram = pgTable("telegram", {
.$defaultFn(() => nanoid()),
botToken: text("botToken").notNull(),
chatId: text("chatId").notNull(),
messageThreadId: text("messageThreadId"),
});
export const discord = pgTable("discord", {
@@ -121,9 +122,9 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.gotifyId],
references: [gotify.gotifyId],
}),
admin: one(admins, {
fields: [notifications.adminId],
references: [admins.adminId],
organization: one(organization, {
fields: [notifications.organizationId],
references: [organization.id],
}),
}));
@@ -148,7 +149,7 @@ export const apiCreateSlack = notificationsSchema
export const apiUpdateSlack = apiCreateSlack.partial().extend({
notificationId: z.string().min(1),
slackId: z.string(),
adminId: z.string().optional(),
organizationId: z.string().optional(),
});
export const apiTestSlackConnection = apiCreateSlack.pick({
@@ -169,18 +170,20 @@ export const apiCreateTelegram = notificationsSchema
.extend({
botToken: z.string().min(1),
chatId: z.string().min(1),
messageThreadId: z.string(),
})
.required();
export const apiUpdateTelegram = apiCreateTelegram.partial().extend({
notificationId: z.string().min(1),
telegramId: z.string().min(1),
adminId: z.string().optional(),
organizationId: z.string().optional(),
});
export const apiTestTelegramConnection = apiCreateTelegram.pick({
botToken: true,
chatId: true,
messageThreadId: true,
});
export const apiCreateDiscord = notificationsSchema
@@ -202,7 +205,7 @@ export const apiCreateDiscord = notificationsSchema
export const apiUpdateDiscord = apiCreateDiscord.partial().extend({
notificationId: z.string().min(1),
discordId: z.string().min(1),
adminId: z.string().optional(),
organizationId: z.string().optional(),
});
export const apiTestDiscordConnection = apiCreateDiscord
@@ -236,7 +239,7 @@ export const apiCreateEmail = notificationsSchema
export const apiUpdateEmail = apiCreateEmail.partial().extend({
notificationId: z.string().min(1),
emailId: z.string().min(1),
adminId: z.string().optional(),
organizationId: z.string().optional(),
});
export const apiTestEmailConnection = apiCreateEmail.pick({
@@ -268,7 +271,7 @@ export const apiCreateGotify = notificationsSchema
export const apiUpdateGotify = apiCreateGotify.partial().extend({
notificationId: z.string().min(1),
gotifyId: z.string().min(1),
adminId: z.string().optional(),
organizationId: z.string().optional(),
});
export const apiTestGotifyConnection = apiCreateGotify

View File

@@ -140,3 +140,9 @@ export const apiUpdatePostgres = createSchema
postgresId: z.string().min(1),
})
.omit({ serverId: true });
export const apiRebuildPostgres = createSchema
.pick({
postgresId: true,
})
.required();

View File

@@ -1,10 +1,9 @@
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 { admins } from "./admin";
import { organization } from "./account";
import { applications } from "./application";
import { compose } from "./compose";
import { mariadb } from "./mariadb";
@@ -23,9 +22,10 @@ export const projects = pgTable("project", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
adminId: text("adminId")
organizationId: text("organizationId")
.notNull()
.references(() => admins.adminId, { onDelete: "cascade" }),
.references(() => organization.id, { onDelete: "cascade" }),
env: text("env").notNull().default(""),
});
@@ -37,9 +37,9 @@ export const projectRelations = relations(projects, ({ many, one }) => ({
mongo: many(mongo),
redis: many(redis),
compose: many(compose),
admin: one(admins, {
fields: [projects.adminId],
references: [admins.adminId],
organization: one(organization, {
fields: [projects.organizationId],
references: [organization.id],
}),
}));

View File

@@ -133,3 +133,9 @@ export const apiUpdateRedis = createSchema
redisId: z.string().min(1),
})
.omit({ serverId: true });
export const apiRebuildRedis = createSchema
.pick({
redisId: true,
})
.required();

View File

@@ -3,7 +3,7 @@ import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { admins } from "./admin";
import { organization } from "./account";
import { applications } from "./application";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
@@ -27,16 +27,12 @@ export const registry = pgTable("registry", {
.notNull()
.$defaultFn(() => new Date().toISOString()),
registryType: registryType("selfHosted").notNull().default("cloud"),
adminId: text("adminId")
organizationId: text("organizationId")
.notNull()
.references(() => admins.adminId, { onDelete: "cascade" }),
.references(() => organization.id, { onDelete: "cascade" }),
});
export const registryRelations = relations(registry, ({ one, many }) => ({
admin: one(admins, {
fields: [registry.adminId],
references: [admins.adminId],
}),
export const registryRelations = relations(registry, ({ many }) => ({
applications: many(applications),
}));
@@ -45,7 +41,7 @@ const createSchema = createInsertSchema(registry, {
username: z.string().min(1),
password: z.string().min(1),
registryUrl: z.string(),
adminId: z.string().min(1),
organizationId: z.string().min(1),
registryId: z.string().min(1),
registryType: z.enum(["cloud"]),
imagePrefix: z.string().nullable().optional(),

View File

@@ -0,0 +1,872 @@
enum applicationStatus {
idle
running
done
error
}
enum buildType {
dockerfile
heroku_buildpacks
paketo_buildpacks
nixpacks
static
}
enum certificateType {
letsencrypt
none
}
enum composeType {
"docker-compose"
stack
}
enum databaseType {
postgres
mariadb
mysql
mongo
}
enum deploymentStatus {
running
done
error
}
enum domainType {
compose
application
preview
}
enum gitProviderType {
github
gitlab
bitbucket
}
enum mountType {
bind
volume
file
}
enum notificationType {
slack
telegram
discord
email
gotify
}
enum protocolType {
tcp
udp
}
enum RegistryType {
selfHosted
cloud
}
enum Roles {
admin
user
}
enum serverStatus {
active
inactive
}
enum serviceType {
application
postgres
mysql
mariadb
mongo
redis
compose
}
enum sourceType {
docker
git
github
gitlab
bitbucket
drop
}
enum sourceTypeCompose {
git
github
gitlab
bitbucket
raw
}
table account {
id text [pk, not null]
account_id text [not null]
provider_id text [not null]
user_id text [not null]
access_token text
refresh_token text
id_token text
access_token_expires_at timestamp
refresh_token_expires_at timestamp
scope text
password text
is2FAEnabled boolean [not null, default: false]
created_at timestamp [not null]
updated_at timestamp [not null]
resetPasswordToken text
resetPasswordExpiresAt text
confirmationToken text
confirmationExpiresAt text
}
table admin {
}
table application {
applicationId text [pk, not null]
name text [not null]
appName text [not null, unique]
description text
env text
previewEnv text
previewBuildArgs text
previewWildcard text
previewPort integer [default: 3000]
previewHttps boolean [not null, default: false]
previewPath text [default: '/']
certificateType certificateType [not null, default: 'none']
previewLimit integer [default: 3]
isPreviewDeploymentsActive boolean [default: false]
buildArgs text
memoryReservation text
memoryLimit text
cpuReservation text
cpuLimit text
title text
enabled boolean
subtitle text
command text
refreshToken text
sourceType sourceType [not null, default: 'github']
repository text
owner text
branch text
buildPath text [default: '/']
autoDeploy boolean
gitlabProjectId integer
gitlabRepository text
gitlabOwner text
gitlabBranch text
gitlabBuildPath text [default: '/']
gitlabPathNamespace text
bitbucketRepository text
bitbucketOwner text
bitbucketBranch text
bitbucketBuildPath text [default: '/']
username text
password text
dockerImage text
registryUrl text
customGitUrl text
customGitBranch text
customGitBuildPath text
customGitSSHKeyId text
dockerfile text
dockerContextPath text
dockerBuildStage text
dropBuildPath text
healthCheckSwarm json
restartPolicySwarm json
placementSwarm json
updateConfigSwarm json
rollbackConfigSwarm json
modeSwarm json
labelsSwarm json
networkSwarm json
replicas integer [not null, default: 1]
applicationStatus applicationStatus [not null, default: 'idle']
buildType buildType [not null, default: 'nixpacks']
herokuVersion text [default: '24']
publishDirectory text
createdAt text [not null]
registryId text
projectId text [not null]
githubId text
gitlabId text
bitbucketId text
serverId text
}
table auth {
id text [pk, not null]
email text [not null, unique]
password text [not null]
rol Roles [not null]
image text
secret text
token text
is2FAEnabled boolean [not null, default: false]
createdAt text [not null]
resetPasswordToken text
resetPasswordExpiresAt text
confirmationToken text
confirmationExpiresAt text
}
table backup {
backupId text [pk, not null]
schedule text [not null]
enabled boolean
database text [not null]
prefix text [not null]
destinationId text [not null]
databaseType databaseType [not null]
postgresId text
mariadbId text
mysqlId text
mongoId text
}
table bitbucket {
bitbucketId text [pk, not null]
bitbucketUsername text
appPassword text
bitbucketWorkspaceName text
gitProviderId text [not null]
}
table certificate {
certificateId text [pk, not null]
name text [not null]
certificateData text [not null]
privateKey text [not null]
certificatePath text [not null, unique]
autoRenew boolean
userId text
serverId text
}
table compose {
composeId text [pk, not null]
name text [not null]
appName text [not null]
description text
env text
composeFile text [not null, default: '']
refreshToken text
sourceType sourceTypeCompose [not null, default: 'github']
composeType composeType [not null, default: 'docker-compose']
repository text
owner text
branch text
autoDeploy boolean
gitlabProjectId integer
gitlabRepository text
gitlabOwner text
gitlabBranch text
gitlabPathNamespace text
bitbucketRepository text
bitbucketOwner text
bitbucketBranch text
customGitUrl text
customGitBranch text
customGitSSHKeyId text
command text [not null, default: '']
composePath text [not null, default: './docker-compose.yml']
suffix text [not null, default: '']
randomize boolean [not null, default: false]
isolatedDeployment boolean [not null, default: false]
composeStatus applicationStatus [not null, default: 'idle']
projectId text [not null]
createdAt text [not null]
githubId text
gitlabId text
bitbucketId text
serverId text
}
table deployment {
deploymentId text [pk, not null]
title text [not null]
description text
status deploymentStatus [default: 'running']
logPath text [not null]
applicationId text
composeId text
serverId text
isPreviewDeployment boolean [default: false]
previewDeploymentId text
createdAt text [not null]
errorMessage text
}
table destination {
destinationId text [pk, not null]
name text [not null]
provider text
accessKey text [not null]
secretAccessKey text [not null]
bucket text [not null]
region text [not null]
endpoint text [not null]
userId text [not null]
}
table discord {
discordId text [pk, not null]
webhookUrl text [not null]
decoration boolean
}
table domain {
domainId text [pk, not null]
host text [not null]
https boolean [not null, default: false]
port integer [default: 3000]
path text [default: '/']
serviceName text
domainType domainType [default: 'application']
uniqueConfigKey serial [not null, increment]
createdAt text [not null]
composeId text
applicationId text
previewDeploymentId text
certificateType certificateType [not null, default: 'none']
}
table email {
emailId text [pk, not null]
smtpServer text [not null]
smtpPort integer [not null]
username text [not null]
password text [not null]
fromAddress text [not null]
toAddress text[] [not null]
}
table git_provider {
gitProviderId text [pk, not null]
name text [not null]
providerType gitProviderType [not null, default: 'github']
createdAt text [not null]
userId text
}
table github {
githubId text [pk, not null]
githubAppName text
githubAppId integer
githubClientId text
githubClientSecret text
githubInstallationId text
githubPrivateKey text
githubWebhookSecret text
gitProviderId text [not null]
}
table gitlab {
gitlabId text [pk, not null]
gitlabUrl text [not null, default: 'https://gitlab.com']
application_id text
redirect_uri text
secret text
access_token text
refresh_token text
group_name text
expires_at integer
gitProviderId text [not null]
}
table gotify {
gotifyId text [pk, not null]
serverUrl text [not null]
appToken text [not null]
priority integer [not null, default: 5]
decoration boolean
}
table invitation {
id text [pk, not null]
organization_id text [not null]
email text [not null]
role text
status text [not null]
expires_at timestamp [not null]
inviter_id text [not null]
}
table mariadb {
mariadbId text [pk, not null]
name text [not null]
appName text [not null, unique]
description text
databaseName text [not null]
databaseUser text [not null]
databasePassword text [not null]
rootPassword text [not null]
dockerImage text [not null]
command text
env text
memoryReservation text
memoryLimit text
cpuReservation text
cpuLimit text
externalPort integer
applicationStatus applicationStatus [not null, default: 'idle']
createdAt text [not null]
projectId text [not null]
serverId text
}
table member {
id text [pk, not null]
organization_id text [not null]
user_id text [not null]
role text [not null]
created_at timestamp [not null]
}
table mongo {
mongoId text [pk, not null]
name text [not null]
appName text [not null, unique]
description text
databaseUser text [not null]
databasePassword text [not null]
dockerImage text [not null]
command text
env text
memoryReservation text
memoryLimit text
cpuReservation text
cpuLimit text
externalPort integer
applicationStatus applicationStatus [not null, default: 'idle']
createdAt text [not null]
projectId text [not null]
serverId text
replicaSets boolean [default: false]
}
table mount {
mountId text [pk, not null]
type mountType [not null]
hostPath text
volumeName text
filePath text
content text
serviceType serviceType [not null, default: 'application']
mountPath text [not null]
applicationId text
postgresId text
mariadbId text
mongoId text
mysqlId text
redisId text
composeId text
}
table mysql {
mysqlId text [pk, not null]
name text [not null]
appName text [not null, unique]
description text
databaseName text [not null]
databaseUser text [not null]
databasePassword text [not null]
rootPassword text [not null]
dockerImage text [not null]
command text
env text
memoryReservation text
memoryLimit text
cpuReservation text
cpuLimit text
externalPort integer
applicationStatus applicationStatus [not null, default: 'idle']
createdAt text [not null]
projectId text [not null]
serverId text
}
table notification {
notificationId text [pk, not null]
name text [not null]
appDeploy boolean [not null, default: false]
appBuildError boolean [not null, default: false]
databaseBackup boolean [not null, default: false]
dokployRestart boolean [not null, default: false]
dockerCleanup boolean [not null, default: false]
serverThreshold boolean [not null, default: false]
notificationType notificationType [not null]
createdAt text [not null]
slackId text
telegramId text
discordId text
emailId text
gotifyId text
userId text
}
table organization {
id text [pk, not null]
name text [not null]
slug text [unique]
logo text
created_at timestamp [not null]
metadata text
owner_id text [not null]
}
table port {
portId text [pk, not null]
publishedPort integer [not null]
targetPort integer [not null]
protocol protocolType [not null]
applicationId text [not null]
}
table postgres {
postgresId text [pk, not null]
name text [not null]
appName text [not null, unique]
databaseName text [not null]
databaseUser text [not null]
databasePassword text [not null]
description text
dockerImage text [not null]
command text
env text
memoryReservation text
externalPort integer
memoryLimit text
cpuReservation text
cpuLimit text
applicationStatus applicationStatus [not null, default: 'idle']
createdAt text [not null]
projectId text [not null]
serverId text
}
table preview_deployments {
previewDeploymentId text [pk, not null]
branch text [not null]
pullRequestId text [not null]
pullRequestNumber text [not null]
pullRequestURL text [not null]
pullRequestTitle text [not null]
pullRequestCommentId text [not null]
previewStatus applicationStatus [not null, default: 'idle']
appName text [not null, unique]
applicationId text [not null]
domainId text
createdAt text [not null]
expiresAt text
}
table project {
projectId text [pk, not null]
name text [not null]
description text
createdAt text [not null]
userId text [not null]
env text [not null, default: '']
}
table redirect {
redirectId text [pk, not null]
regex text [not null]
replacement text [not null]
permanent boolean [not null, default: false]
uniqueConfigKey serial [not null, increment]
createdAt text [not null]
applicationId text [not null]
}
table redis {
redisId text [pk, not null]
name text [not null]
appName text [not null, unique]
description text
password text [not null]
dockerImage text [not null]
command text
env text
memoryReservation text
memoryLimit text
cpuReservation text
cpuLimit text
externalPort integer
createdAt text [not null]
applicationStatus applicationStatus [not null, default: 'idle']
projectId text [not null]
serverId text
}
table registry {
registryId text [pk, not null]
registryName text [not null]
imagePrefix text
username text [not null]
password text [not null]
registryUrl text [not null, default: '']
createdAt text [not null]
selfHosted RegistryType [not null, default: 'cloud']
userId text [not null]
}
table security {
securityId text [pk, not null]
username text [not null]
password text [not null]
createdAt text [not null]
applicationId text [not null]
indexes {
(username, applicationId) [name: 'security_username_applicationId_unique', unique]
}
}
table server {
serverId text [pk, not null]
name text [not null]
description text
ipAddress text [not null]
port integer [not null]
username text [not null, default: 'root']
appName text [not null]
enableDockerCleanup boolean [not null, default: false]
createdAt text [not null]
userId text [not null]
serverStatus serverStatus [not null, default: 'active']
command text [not null, default: '']
sshKeyId text
metricsConfig jsonb [not null, default: `{"server":{"type":"Remote","refreshRate":60,"port":4500,"token":"","urlCallback":"","cronJob":"","retentionDays":2,"thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
}
table session {
id text [pk, not null]
expires_at timestamp [not null]
token text [not null, unique]
created_at timestamp [not null]
updated_at timestamp [not null]
ip_address text
user_agent text
user_id text [not null]
impersonated_by text
active_organization_id text
}
table slack {
slackId text [pk, not null]
webhookUrl text [not null]
channel text
}
table "ssh-key" {
sshKeyId text [pk, not null]
privateKey text [not null, default: '']
publicKey text [not null]
name text [not null]
description text
createdAt text [not null]
lastUsedAt text
userId text
}
table telegram {
telegramId text [pk, not null]
botToken text [not null]
chatId text [not null]
}
table user {
id text [pk, not null]
name text [not null, default: '']
token text [not null]
isRegistered boolean [not null, default: false]
expirationDate text [not null]
createdAt text [not null]
canCreateProjects boolean [not null, default: false]
canAccessToSSHKeys boolean [not null, default: false]
canCreateServices boolean [not null, default: false]
canDeleteProjects boolean [not null, default: false]
canDeleteServices boolean [not null, default: false]
canAccessToDocker boolean [not null, default: false]
canAccessToAPI boolean [not null, default: false]
canAccessToGitProviders boolean [not null, default: false]
canAccessToTraefikFiles boolean [not null, default: false]
accesedProjects text[] [not null, default: `ARRAY[]::text[]`]
accesedServices text[] [not null, default: `ARRAY[]::text[]`]
email text [not null, unique]
email_verified boolean [not null]
image text
role text
banned boolean
ban_reason text
ban_expires timestamp
updated_at timestamp [not null]
serverIp text
certificateType certificateType [not null, default: 'none']
host text
letsEncryptEmail text
sshPrivateKey text
enableDockerCleanup boolean [not null, default: false]
enableLogRotation boolean [not null, default: false]
enablePaidFeatures boolean [not null, default: false]
metricsConfig jsonb [not null, default: `{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}`]
cleanupCacheApplications boolean [not null, default: false]
cleanupCacheOnPreviews boolean [not null, default: false]
cleanupCacheOnCompose boolean [not null, default: false]
stripeCustomerId text
stripeSubscriptionId text
serversQuantity integer [not null, default: 0]
}
table verification {
id text [pk, not null]
identifier text [not null]
value text [not null]
expires_at timestamp [not null]
created_at timestamp
updated_at timestamp
}
ref: mount.applicationId > application.applicationId
ref: mount.postgresId > postgres.postgresId
ref: mount.mariadbId > mariadb.mariadbId
ref: mount.mongoId > mongo.mongoId
ref: mount.mysqlId > mysql.mysqlId
ref: mount.redisId > redis.redisId
ref: mount.composeId > compose.composeId
ref: application.projectId > project.projectId
ref: application.customGitSSHKeyId > "ssh-key".sshKeyId
ref: application.registryId > registry.registryId
ref: application.githubId - github.githubId
ref: application.gitlabId - gitlab.gitlabId
ref: application.bitbucketId - bitbucket.bitbucketId
ref: application.serverId > server.serverId
ref: backup.destinationId > destination.destinationId
ref: backup.postgresId > postgres.postgresId
ref: backup.mariadbId > mariadb.mariadbId
ref: backup.mysqlId > mysql.mysqlId
ref: backup.mongoId > mongo.mongoId
ref: git_provider.gitProviderId - bitbucket.gitProviderId
ref: certificate.serverId > server.serverId
ref: certificate.userId - user.id
ref: compose.projectId > project.projectId
ref: compose.customGitSSHKeyId > "ssh-key".sshKeyId
ref: compose.githubId - github.githubId
ref: compose.gitlabId - gitlab.gitlabId
ref: compose.bitbucketId - bitbucket.bitbucketId
ref: compose.serverId > server.serverId
ref: deployment.applicationId > application.applicationId
ref: deployment.composeId > compose.composeId
ref: deployment.serverId > server.serverId
ref: deployment.previewDeploymentId > preview_deployments.previewDeploymentId
ref: destination.userId - user.id
ref: domain.applicationId > application.applicationId
ref: domain.composeId > compose.composeId
ref: preview_deployments.domainId - domain.domainId
ref: github.gitProviderId - git_provider.gitProviderId
ref: gitlab.gitProviderId - git_provider.gitProviderId
ref: git_provider.userId - user.id
ref: mariadb.projectId > project.projectId
ref: mariadb.serverId > server.serverId
ref: mongo.projectId > project.projectId
ref: mongo.serverId > server.serverId
ref: mysql.projectId > project.projectId
ref: mysql.serverId > server.serverId
ref: notification.slackId - slack.slackId
ref: notification.telegramId - telegram.telegramId
ref: notification.discordId - discord.discordId
ref: notification.emailId - email.emailId
ref: notification.gotifyId - gotify.gotifyId
ref: notification.userId - user.id
ref: port.applicationId > application.applicationId
ref: postgres.projectId > project.projectId
ref: postgres.serverId > server.serverId
ref: preview_deployments.applicationId > application.applicationId
ref: project.userId - user.id
ref: redirect.applicationId > application.applicationId
ref: redis.projectId > project.projectId
ref: redis.serverId > server.serverId
ref: registry.userId - user.id
ref: security.applicationId > application.applicationId
ref: server.userId - user.id
ref: server.sshKeyId > "ssh-key".sshKeyId
ref: "ssh-key".userId - user.id

View File

@@ -10,8 +10,7 @@ import {
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { admins } from "./admin";
import { organization } from "./account";
import { applications } from "./application";
import { certificates } from "./certificate";
import { compose } from "./compose";
@@ -40,12 +39,10 @@ export const server = pgTable("server", {
.notNull()
.$defaultFn(() => generateAppName("server")),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
createdAt: text("createdAt")
createdAt: text("createdAt").notNull(),
organizationId: text("organizationId")
.notNull()
.$defaultFn(() => new Date().toISOString()),
adminId: text("adminId")
.notNull()
.references(() => admins.adminId, { onDelete: "cascade" }),
.references(() => organization.id, { onDelete: "cascade" }),
serverStatus: serverStatus("serverStatus").notNull().default("active"),
command: text("command").notNull().default(""),
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
@@ -100,10 +97,6 @@ export const server = pgTable("server", {
});
export const serverRelations = relations(server, ({ one, many }) => ({
admin: one(admins, {
fields: [server.adminId],
references: [admins.adminId],
}),
deployments: many(deployments),
sshKey: one(sshKeys, {
fields: [server.sshKeyId],
@@ -117,6 +110,10 @@ export const serverRelations = relations(server, ({ one, many }) => ({
mysql: many(mysql),
postgres: many(postgres),
certificates: many(certificates),
organization: one(organization, {
fields: [server.organizationId],
references: [organization.id],
}),
}));
const createSchema = createInsertSchema(server, {

View File

@@ -1,13 +1,18 @@
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { auth } from "./auth";
import { users_temp } from "./user";
export const sessionTable = pgTable("session", {
// OLD TABLE
export const session = pgTable("session_temp", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => auth.id, { onDelete: "cascade" }),
expiresAt: timestamp("expires_at", {
withTimezone: true,
mode: "date",
}).notNull(),
.references(() => users_temp.id, { onDelete: "cascade" }),
impersonatedBy: text("impersonated_by"),
activeOrganizationId: text("active_organization_id"),
});

View File

@@ -10,4 +10,5 @@ export const applicationStatus = pgEnum("applicationStatus", [
export const certificateType = pgEnum("certificateType", [
"letsencrypt",
"none",
"custom",
]);

View File

@@ -1,27 +0,0 @@
import { pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
export const source = pgTable("project", {
projectId: text("projectId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
description: text("description"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
});
const createSchema = createInsertSchema(source, {
name: z.string().min(1),
description: z.string(),
projectId: z.string(),
});
export const apiCreate = createSchema.pick({
name: true,
description: true,
});

View File

@@ -3,7 +3,7 @@ import { pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { sshKeyCreate, sshKeyType } from "../validations";
import { admins } from "./admin";
import { organization } from "./account";
import { applications } from "./application";
import { compose } from "./compose";
import { server } from "./server";
@@ -21,18 +21,18 @@ export const sshKeys = pgTable("ssh-key", {
.notNull()
.$defaultFn(() => new Date().toISOString()),
lastUsedAt: text("lastUsedAt"),
adminId: text("adminId").references(() => admins.adminId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
});
export const sshKeysRelations = relations(sshKeys, ({ many, one }) => ({
applications: many(applications),
compose: many(compose),
servers: many(server),
admin: one(admins, {
fields: [sshKeys.adminId],
references: [admins.adminId],
organization: one(organization, {
fields: [sshKeys.organizationId],
references: [organization.id],
}),
}));
@@ -48,7 +48,7 @@ export const apiCreateSshKey = createSchema
description: true,
privateKey: true,
publicKey: true,
adminId: true,
organizationId: true,
})
.merge(sshKeyCreate.pick({ privateKey: true }));

View File

@@ -1,10 +1,18 @@
import { relations, sql } from "drizzle-orm";
import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import {
boolean,
integer,
jsonb,
pgTable,
text,
timestamp,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { admins } from "./admin";
import { auth } from "./auth";
import { account, organization, apikey } from "./account";
import { projects } from "./project";
import { certificateType } from "./shared";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@@ -12,75 +20,115 @@ import { auth } from "./auth";
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
*/
export const users = pgTable("user", {
userId: text("userId")
// OLD TABLE
// TEMP
export const users_temp = pgTable("user_temp", {
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
token: text("token").notNull(),
name: text("name").notNull().default(""),
isRegistered: boolean("isRegistered").notNull().default(false),
expirationDate: timestamp("expirationDate", {
precision: 3,
mode: "string",
}).notNull(),
createdAt: text("createdAt")
expirationDate: text("expirationDate")
.notNull()
.$defaultFn(() => new Date().toISOString()),
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
canCreateServices: boolean("canCreateServices").notNull().default(false),
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
canAccessToAPI: boolean("canAccessToAPI").notNull().default(false),
canAccessToGitProviders: boolean("canAccessToGitProviders")
createdAt2: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
createdAt: timestamp("created_at").defaultNow(),
// Auth
twoFactorEnabled: boolean("two_factor_enabled"),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").notNull(),
image: text("image"),
banned: boolean("banned"),
banReason: text("ban_reason"),
banExpires: timestamp("ban_expires"),
updatedAt: timestamp("updated_at").notNull(),
// Admin
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
logCleanupCron: text("logCleanupCron"),
// Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
accessedProjects: text("accesedProjects")
.array()
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(sql`ARRAY[]::text[]`),
accessedServices: text("accesedServices")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
adminId: text("adminId")
.notNull()
.references(() => admins.adminId, { onDelete: "cascade" }),
authId: text("authId")
.notNull()
.references(() => auth.id, { onDelete: "cascade" }),
.default(false),
stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0),
});
export const usersRelations = relations(users, ({ one }) => ({
auth: one(auth, {
fields: [users.authId],
references: [auth.id],
}),
admin: one(admins, {
fields: [users.adminId],
references: [admins.adminId],
export const usersRelations = relations(users_temp, ({ one, many }) => ({
account: one(account, {
fields: [users_temp.id],
references: [account.userId],
}),
organizations: many(organization),
projects: many(projects),
apiKeys: many(apikey),
}));
const createSchema = createInsertSchema(users, {
userId: z.string().min(1),
authId: z.string().min(1),
token: z.string().min(1),
const createSchema = createInsertSchema(users_temp, {
id: z.string().min(1),
isRegistered: z.boolean().optional(),
adminId: z.string(),
accessedProjects: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional(),
canCreateServices: z.boolean().optional(),
canDeleteProjects: z.boolean().optional(),
canDeleteServices: z.boolean().optional(),
canAccessToDocker: z.boolean().optional(),
canAccessToTraefikFiles: z.boolean().optional(),
});
export const apiCreateUserInvitation = createSchema.pick({}).extend({
@@ -89,41 +137,179 @@ export const apiCreateUserInvitation = createSchema.pick({}).extend({
export const apiRemoveUser = createSchema
.pick({
authId: true,
id: true,
})
.required();
export const apiFindOneToken = createSchema
.pick({
token: true,
})
.required();
.pick({})
.required()
.extend({
token: z.string().min(1),
});
export const apiAssignPermissions = createSchema
.pick({
userId: true,
canCreateProjects: true,
canCreateServices: true,
canDeleteProjects: true,
canDeleteServices: true,
accessedProjects: true,
accessedServices: true,
canAccessToTraefikFiles: true,
canAccessToDocker: true,
canAccessToAPI: true,
canAccessToSSHKeys: true,
canAccessToGitProviders: true,
id: true,
// canCreateProjects: true,
// canCreateServices: true,
// canDeleteProjects: true,
// canDeleteServices: true,
// accessedProjects: true,
// accessedServices: true,
// canAccessToTraefikFiles: true,
// canAccessToDocker: true,
// canAccessToAPI: true,
// canAccessToSSHKeys: true,
// canAccessToGitProviders: true,
})
.extend({
accessedProjects: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional(),
canCreateServices: z.boolean().optional(),
canDeleteProjects: z.boolean().optional(),
canDeleteServices: z.boolean().optional(),
canAccessToDocker: z.boolean().optional(),
canAccessToTraefikFiles: z.boolean().optional(),
canAccessToAPI: z.boolean().optional(),
canAccessToSSHKeys: z.boolean().optional(),
canAccessToGitProviders: z.boolean().optional(),
})
.required();
export const apiFindOneUser = createSchema
.pick({
userId: true,
id: true,
})
.required();
export const apiFindOneUserByAuth = createSchema
.pick({
authId: true,
// authId: true,
})
.required();
export const apiSaveSSHKey = createSchema
.pick({
sshPrivateKey: true,
})
.required();
export const apiAssignDomain = createSchema
.pick({
host: true,
certificateType: true,
letsEncryptEmail: true,
})
.required()
.partial({
letsEncryptEmail: true,
});
export const apiUpdateDockerCleanup = createSchema
.pick({
enableDockerCleanup: true,
})
.required()
.extend({
serverId: z.string().optional(),
});
export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1),
});
export const apiModifyTraefikConfig = z.object({
path: z.string().min(1),
traefikConfig: z.string().min(1),
serverId: z.string().optional(),
});
export const apiReadTraefikConfig = z.object({
path: z.string().min(1),
serverId: z.string().optional(),
});
export const apiEnableDashboard = z.object({
enableDashboard: z.boolean().optional(),
serverId: z.string().optional(),
});
export const apiServerSchema = z
.object({
serverId: z.string().optional(),
})
.optional();
export const apiReadStatsLogs = z.object({
page: z
.object({
pageIndex: z.number(),
pageSize: z.number(),
})
.optional(),
status: z.string().array().optional(),
search: z.string().optional(),
sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
dateRange: z
.object({
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});
export const apiUpdateUser = createSchema.partial().extend({
password: z.string().optional(),
currentPassword: z.string().optional(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
logCleanupCron: z.string().optional().nullable(),
});

View File

@@ -10,7 +10,8 @@ export const domain = z
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
customCertResolver: z.string(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
@@ -20,6 +21,14 @@ export const domain = z
message: "Required",
});
}
if (input.certificateType === "custom" && !input.customCertResolver) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["customCertResolver"],
message: "Required when certificate type is custom",
});
}
});
export const domainCompose = z
@@ -32,7 +41,8 @@ export const domainCompose = z
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
customCertResolver: z.string(),
serviceName: z.string().min(1, { message: "Service name is required" }),
})
.superRefine((input, ctx) => {
@@ -43,4 +53,12 @@ export const domainCompose = z
message: "Required",
});
}
if (input.certificateType === "custom" && !input.customCertResolver) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["customCertResolver"],
message: "Required when certificate type is custom",
});
}
});

View File

@@ -12,7 +12,6 @@ import {
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
export type TemplateProps = {
projectName: string;

View File

@@ -12,7 +12,6 @@ import {
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
export type TemplateProps = {
projectName: string;

View File

@@ -10,7 +10,6 @@ import {
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
export type TemplateProps = {
projectName: string;

View File

@@ -1,6 +1,5 @@
import {
Body,
Button,
Container,
Head,
Heading,
@@ -11,7 +10,6 @@ import {
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
export type TemplateProps = {
message: string;

View File

@@ -10,7 +10,6 @@ import {
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
export type TemplateProps = {
date: string;

View File

@@ -9,7 +9,6 @@ import {
Preview,
Text,
} from "@react-email/components";
import * as React from "react";
interface NotionMagicLinkEmailProps {
loginCode?: string;

View File

@@ -9,7 +9,6 @@ import {
Section,
Text,
} from "@react-email/components";
import * as React from "react";
interface PlaidVerifyIdentityEmailProps {
validationCode?: string;

View File

@@ -11,7 +11,6 @@ import {
Section,
Text,
} from "@react-email/components";
import * as React from "react";
const baseUrl = process.env.VERCEL_URL!;

View File

@@ -15,7 +15,6 @@ import {
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
interface VercelInviteUserEmailProps {
username?: string;

View File

@@ -1,7 +1,4 @@
export * from "./auth/auth";
export * from "./auth/token";
export * from "./auth/random-password";
// export * from "./db";
export * from "./services/admin";
export * from "./services/user";
export * from "./services/project";
@@ -30,11 +27,10 @@ export * from "./services/ssh-key";
export * from "./services/git-provider";
export * from "./services/bitbucket";
export * from "./services/github";
export * from "./services/auth";
export * from "./services/gitlab";
export * from "./services/server";
export * from "./services/application";
export * from "./utils/databases/rebuild";
export * from "./setup/config-paths";
export * from "./setup/postgres-setup";
export * from "./setup/redis-setup";
@@ -44,7 +40,7 @@ export * from "./setup/setup";
export * from "./setup/traefik-setup";
export * from "./setup/server-validate";
export * from "./setup/server-audit";
export * from "./utils/watch-paths/should-deploy";
export * from "./utils/backups/index";
export * from "./utils/backups/mariadb";
export * from "./utils/backups/mongo";
@@ -118,3 +114,11 @@ export * from "./monitoring/utils";
export * from "./db/validations/domain";
export * from "./db/validations/index";
export * from "./utils/gpu-setup";
export * from "./lib/auth";
export {
startLogCleanup,
stopLogCleanup,
getLogCleanupStatus,
} from "./utils/access-log/handler";

View File

@@ -0,0 +1,304 @@
import type { IncomingMessage } from "node:http";
import * as bcrypt from "bcrypt";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { organization, twoFactor, apiKey } from "better-auth/plugins";
import { and, desc, eq } from "drizzle-orm";
import { db } from "../db";
import * as schema from "../db/schema";
import { sendEmail } from "../verification/send-verification-email";
import { IS_CLOUD } from "../constants";
const { handler, api } = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: schema,
}),
logger: {
disabled: process.env.NODE_ENV === "production",
},
appName: "Dokploy",
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }) => {
if (IS_CLOUD) {
await sendEmail({
email: user.email,
subject: "Verify your email",
text: `
<p>Click the link to verify your email: <a href="${url}">Verify Email</a></p>
`,
});
}
},
},
emailAndPassword: {
enabled: true,
autoSignIn: !IS_CLOUD,
requireEmailVerification: IS_CLOUD,
password: {
async hash(password) {
return bcrypt.hashSync(password, 10);
},
async verify({ hash, password }) {
return bcrypt.compareSync(password, hash);
},
},
sendResetPassword: async ({ user, url }) => {
await sendEmail({
email: user.email,
subject: "Reset your password",
text: `
<p>Click the link to reset your password: <a href="${url}">Reset Password</a></p>
`,
});
},
},
databaseHooks: {
user: {
create: {
after: async (user) => {
const isAdminPresent = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"),
});
if (IS_CLOUD || !isAdminPresent) {
await db.transaction(async (tx) => {
const organization = await tx
.insert(schema.organization)
.values({
name: "My Organization",
ownerId: user.id,
createdAt: new Date(),
})
.returning()
.then((res) => res[0]);
await tx.insert(schema.member).values({
userId: user.id,
organizationId: organization?.id || "",
role: "owner",
createdAt: new Date(),
});
});
}
},
},
},
session: {
create: {
before: async (session) => {
const member = await db.query.member.findFirst({
where: eq(schema.member.userId, session.userId),
orderBy: desc(schema.member.createdAt),
with: {
organization: true,
},
});
return {
data: {
...session,
activeOrganizationId: member?.organization.id,
},
};
},
},
},
},
user: {
modelName: "users_temp",
additionalFields: {
role: {
type: "string",
// required: true,
input: false,
},
ownerId: {
type: "string",
// required: true,
input: false,
},
},
},
plugins: [
apiKey({
enableMetadata: true,
}),
twoFactor(),
organization({
async sendInvitationEmail(data, _request) {
if (IS_CLOUD) {
const host =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://dokploy.com";
const inviteLink = `${host}/invitation?token=${data.id}`;
await sendEmail({
email: data.email,
subject: "Invitation to join organization",
text: `
<p>You are invited to join ${data.organization.name} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
`,
});
}
},
}),
],
});
export const auth = {
handler,
createApiKey: api.createApiKey,
};
export const validateRequest = async (request: IncomingMessage) => {
const apiKey = request.headers["x-api-key"] as string;
if (apiKey) {
try {
const { valid, key, error } = await api.verifyApiKey({
body: {
key: apiKey,
},
});
if (error) {
throw new Error(error.message || "Error verifying API key");
}
if (!valid || !key) {
return {
session: null,
user: null,
};
}
const apiKeyRecord = await db.query.apikey.findFirst({
where: eq(schema.apikey.id, key.id),
with: {
user: true,
},
});
if (!apiKeyRecord) {
return {
session: null,
user: null,
};
}
const organizationId = JSON.parse(
apiKeyRecord.metadata || "{}",
).organizationId;
if (!organizationId) {
return {
session: null,
user: null,
};
}
const member = await db.query.member.findFirst({
where: and(
eq(schema.member.userId, apiKeyRecord.user.id),
eq(schema.member.organizationId, organizationId),
),
with: {
organization: true,
},
});
const {
id,
name,
email,
emailVerified,
image,
createdAt,
updatedAt,
twoFactorEnabled,
} = apiKeyRecord.user;
const mockSession = {
session: {
user: {
id: apiKeyRecord.user.id,
email: apiKeyRecord.user.email,
name: apiKeyRecord.user.name,
},
activeOrganizationId: organizationId || "",
},
user: {
id,
name,
email,
emailVerified,
image,
createdAt,
updatedAt,
twoFactorEnabled,
role: member?.role || "member",
ownerId: member?.organization.ownerId || apiKeyRecord.user.id,
},
};
return mockSession;
} catch (error) {
console.error("Error verifying API key", error);
return {
session: null,
user: null,
};
}
}
// If no API key, proceed with normal session validation
const session = await api.getSession({
headers: new Headers({
cookie: request.headers.cookie || "",
}),
});
if (!session?.session || !session.user) {
return {
session: null,
user: null,
};
}
if (session?.user) {
const member = await db.query.member.findFirst({
where: and(
eq(schema.member.userId, session.user.id),
eq(
schema.member.organizationId,
session.session.activeOrganizationId || "",
),
),
with: {
organization: true,
},
});
session.user.role = member?.role || "member";
if (member) {
session.user.ownerId = member.organization.ownerId;
} else {
session.user.ownerId = session.user.id;
}
}
return session;
};

View File

@@ -73,7 +73,7 @@ export const readStatsFile = async (
const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`;
const data = await promises.readFile(filePath, "utf-8");
return JSON.parse(data);
} catch (error) {
} catch (_error) {
return [];
}
};
@@ -108,7 +108,7 @@ export const readLastValueStatsFile = async (
const data = await promises.readFile(filePath, "utf-8");
const stats = JSON.parse(data);
return stats[stats.length - 1] || null;
} catch (error) {
} catch (_error) {
return null;
}
};

View File

@@ -1,124 +1,59 @@
import { randomBytes } from "node:crypto";
import { db } from "@dokploy/server/db";
import {
admins,
type apiCreateUserInvitation,
auth,
users,
invitation,
member,
organization,
users_temp,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
export type Admin = typeof admins.$inferSelect;
export const createInvitation = async (
input: typeof apiCreateUserInvitation._type,
adminId: string,
) => {
await db.transaction(async (tx) => {
const result = await tx
.insert(auth)
.values({
email: input.email.toLowerCase(),
rol: "user",
password: bcrypt.hashSync("01231203012312", 10),
})
.returning()
.then((res) => res[0]);
if (!result) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the user",
});
}
const expiresIn24Hours = new Date();
expiresIn24Hours.setDate(expiresIn24Hours.getDate() + 1);
const token = randomBytes(32).toString("hex");
await tx
.insert(users)
.values({
adminId: adminId,
authId: result.id,
token,
expirationDate: expiresIn24Hours.toISOString(),
})
.returning();
export const findUserById = async (userId: string) => {
const user = await db.query.users_temp.findFirst({
where: eq(users_temp.id, userId),
// with: {
// account: true,
// },
});
};
export const findAdminById = async (adminId: string) => {
const admin = await db.query.admins.findFirst({
where: eq(admins.adminId, adminId),
});
if (!admin) {
if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Admin not found",
message: "User not found",
});
}
return admin;
return user;
};
export const updateAdmin = async (
authId: string,
adminData: Partial<Admin>,
) => {
const admin = await db
.update(admins)
.set({
...adminData,
})
.where(eq(admins.authId, authId))
.returning()
.then((res) => res[0]);
return admin;
};
export const updateAdminById = async (
adminId: string,
adminData: Partial<Admin>,
) => {
const admin = await db
.update(admins)
.set({
...adminData,
})
.where(eq(admins.adminId, adminId))
.returning()
.then((res) => res[0]);
return admin;
export const findOrganizationById = async (organizationId: string) => {
const organizationResult = await db.query.organization.findFirst({
where: eq(organization.id, organizationId),
with: {
owner: true,
},
});
return organizationResult;
};
export const isAdminPresent = async () => {
const admin = await db.query.admins.findFirst();
const admin = await db.query.member.findFirst({
where: eq(member.role, "owner"),
});
if (!admin) {
return false;
}
return true;
};
export const findAdminByAuthId = async (authId: string) => {
const admin = await db.query.admins.findFirst({
where: eq(admins.authId, authId),
export const findAdmin = async () => {
const admin = await db.query.member.findFirst({
where: eq(member.role, "owner"),
with: {
users: true,
user: true,
},
});
if (!admin) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Admin not found",
});
}
return admin;
};
export const findAdmin = async () => {
const admin = await db.query.admins.findFirst({});
if (!admin) {
throw new TRPCError({
code: "NOT_FOUND",
@@ -129,14 +64,15 @@ export const findAdmin = async () => {
};
export const getUserByToken = async (token: string) => {
const user = await db.query.users.findFirst({
where: eq(users.token, token),
with: {
auth: {
columns: {
password: false,
},
},
const user = await db.query.invitation.findFirst({
where: eq(invitation.id, token),
columns: {
id: true,
email: true,
status: true,
expiresAt: true,
role: true,
inviterId: true,
},
});
@@ -146,34 +82,23 @@ export const getUserByToken = async (token: string) => {
message: "Invitation not found",
});
}
const userAlreadyExists = await db.query.users_temp.findFirst({
where: eq(users_temp.email, user?.email || ""),
});
const { expiresAt, ...rest } = user;
return {
...user,
isExpired: user.isRegistered,
...rest,
isExpired: user.expiresAt < new Date(),
userAlreadyExists: !!userAlreadyExists,
};
};
export const removeUserByAuthId = async (authId: string) => {
export const removeUserById = async (userId: string) => {
await db
.delete(auth)
.where(eq(auth.id, authId))
.returning()
.then((res) => res[0]);
};
export const removeAdminByAuthId = async (authId: string) => {
const admin = await findAdminByAuthId(authId);
if (!admin) return null;
// First delete all associated users
const users = admin.users;
for (const user of users) {
await removeUserByAuthId(user.authId);
}
// Then delete the auth record which will cascade delete the admin
return await db
.delete(auth)
.where(eq(auth.id, authId))
.delete(users_temp)
.where(eq(users_temp.id, userId))
.returning()
.then((res) => res[0]);
};
@@ -184,8 +109,8 @@ export const getDokployUrl = async () => {
}
const admin = await findAdmin();
if (admin.host) {
return `https://${admin.host}`;
if (admin.user.host) {
return `https://${admin.user.host}`;
}
return `http://${admin.serverIp}:${process.env.PORT}`;
return `http://${admin.user.serverIp}:${process.env.PORT}`;
};

View File

@@ -0,0 +1,212 @@
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 { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { IS_CLOUD } from "../constants";
import { findServerById } from "./server";
import { findOrganizationById } from "./admin";
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
const aiSettings = await db.query.ai.findMany({
where: eq(ai.organizationId, organizationId),
orderBy: desc(ai.createdAt),
});
return aiSettings;
};
export const getAiSettingById = async (aiId: string) => {
const aiSetting = await db.query.ai.findFirst({
where: eq(ai.aiId, aiId),
});
if (!aiSetting) {
throw new TRPCError({
code: "NOT_FOUND",
message: "AI settings not found",
});
}
return aiSetting;
};
export const saveAiSettings = async (organizationId: string, settings: any) => {
const aiId = settings.aiId;
return db
.insert(ai)
.values({
aiId,
organizationId,
...settings,
})
.onConflictDoUpdate({
target: ai.aiId,
set: {
...settings,
},
});
};
export const deleteAiSettings = async (aiId: string) => {
return db.delete(ai).where(eq(ai.aiId, aiId));
};
interface Props {
organizationId: string;
aiId: string;
input: string;
serverId?: string | undefined;
}
export const suggestVariants = async ({
organizationId,
aiId,
input,
serverId,
}: Props) => {
try {
const aiSettings = await getAiSettingById(aiId);
if (!aiSettings || !aiSettings.isEnabled) {
throw new TRPCError({
code: "NOT_FOUND",
message: "AI features are not enabled for this configuration",
});
}
const provider = selectAIProvider(aiSettings);
const model = provider(aiSettings.model);
let ip = "";
if (!IS_CLOUD) {
const organization = await findOrganizationById(organizationId);
ip = organization?.owner.serverIp || "";
}
if (serverId) {
const server = await findServerById(serverId);
ip = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
ip = "127.0.0.1";
}
const { object } = await generateObject({
model,
output: "array",
schema: z.object({
id: z.string(),
name: z.string(),
shortDescription: z.string(),
description: z.string(),
}),
prompt: `
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items), the suggestion
should include id, name, shortDescription, and description. Use slug of title for id.
Important rules for the response:
1. The description field should ONLY contain a plain text description of the project, its features, and use cases
2. Do NOT include any code snippets, configuration examples, or installation instructions in the description
3. The shortDescription should be a single-line summary focusing on the main technologies
User wants to create a new project with the following details, it should be installable in docker and can be docker compose generated for it:
${input}
`,
});
if (object?.length) {
const result = [];
for (const suggestion of object) {
try {
const { object: docker } = await generateObject({
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(),
}),
),
}),
prompt: `
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project.
Return the docker compose as a YAML string and environment variables configuration. Follow these rules:
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
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
Project details:
${suggestion?.description}
`,
});
if (!!docker && !!docker.dockerCompose) {
result.push({
...suggestion,
...docker,
});
}
} catch (error) {
console.error("Error in docker compose generation:", error);
}
}
return result;
}
throw new TRPCError({
code: "NOT_FOUND",
message: "No suggestions found",
});
} catch (error) {
console.error("Error in suggestVariants:", error);
throw error;
}
};

View File

@@ -4,7 +4,6 @@ import {
type apiCreateApplication,
applications,
buildAppName,
cleanAppName,
} from "@dokploy/server/db/schema";
import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
import {
@@ -28,7 +27,6 @@ import {
getCustomGitCloneCommand,
} from "@dokploy/server/utils/providers/git";
import {
authGithub,
cloneGithubRepository,
getGithubCloneCommand,
} from "@dokploy/server/utils/providers/github";
@@ -40,7 +38,7 @@ import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { findAdminById, getDokployUrl } from "./admin";
import { getDokployUrl } from "./admin";
import {
createDeployment,
createDeploymentPreview,
@@ -58,7 +56,6 @@ import {
updatePreviewDeployment,
} from "./preview-deployment";
import { validUniqueServerAppName } from "./project";
import { cleanupFullDocker } from "./settings";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
@@ -185,11 +182,11 @@ export const deployApplication = async ({
});
try {
const admin = await findAdminById(application.project.adminId);
// const admin = await findUserById(application.project.userId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") {
await cloneGithubRepository({
@@ -220,7 +217,7 @@ export const deployApplication = async ({
applicationName: application.name,
applicationType: "application",
buildLink,
adminId: application.project.adminId,
organizationId: application.project.organizationId,
domains: application.domains,
});
} catch (error) {
@@ -233,7 +230,7 @@ export const deployApplication = async ({
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
adminId: application.project.adminId,
organizationId: application.project.organizationId,
});
throw error;
@@ -260,11 +257,11 @@ export const rebuildApplication = async ({
});
try {
const admin = await findAdminById(application.project.adminId);
// const admin = await findUserById(application.project.userId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "gitlab") {
@@ -309,11 +306,11 @@ export const deployRemoteApplication = async ({
try {
if (application.serverId) {
const admin = await findAdminById(application.project.adminId);
// const admin = await findUserById(application.project.userId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
let command = "set -e;";
if (application.sourceType === "github") {
command += await getGithubCloneCommand({
@@ -352,7 +349,7 @@ export const deployRemoteApplication = async ({
applicationName: application.name,
applicationType: "application",
buildLink,
adminId: application.project.adminId,
organizationId: application.project.organizationId,
domains: application.domains,
});
} catch (error) {
@@ -376,7 +373,7 @@ export const deployRemoteApplication = async ({
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
adminId: application.project.adminId,
organizationId: application.project.organizationId,
});
throw error;
@@ -454,11 +451,11 @@ export const deployPreviewApplication = async ({
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
application.buildArgs = application.previewBuildArgs;
const admin = await findAdminById(application.project.adminId);
// const admin = await findUserById(application.project.userId);
if (admin.cleanupCacheOnPreviews) {
await cleanupFullDocker(application?.serverId);
}
// if (admin.cleanupCacheOnPreviews) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") {
await cloneGithubRepository({
@@ -568,11 +565,11 @@ export const deployRemotePreviewApplication = async ({
application.buildArgs = application.previewBuildArgs;
if (application.serverId) {
const admin = await findAdminById(application.project.adminId);
// const admin = await findUserById(application.project.userId);
if (admin.cleanupCacheOnPreviews) {
await cleanupFullDocker(application?.serverId);
}
// if (admin.cleanupCacheOnPreviews) {
// await cleanupFullDocker(application?.serverId);
// }
let command = "set -e;";
if (application.sourceType === "github") {
command += await getGithubCloneCommand({
@@ -637,11 +634,11 @@ export const rebuildRemoteApplication = async ({
try {
if (application.serverId) {
const admin = await findAdminById(application.project.adminId);
// const admin = await findUserById(application.project.userId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType !== "docker") {
let command = "set -e;";
command += getBuildCommand(application, deployment.logPath);

View File

@@ -1,184 +0,0 @@
import { randomBytes } from "node:crypto";
import { db } from "@dokploy/server/db";
import {
admins,
type apiCreateAdmin,
type apiCreateUser,
auth,
users,
} from "@dokploy/server/db/schema";
import { getPublicIpWithFallback } from "@dokploy/server/wss/utils";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { eq } from "drizzle-orm";
import encode from "hi-base32";
import { TOTP } from "otpauth";
import QRCode from "qrcode";
import { IS_CLOUD } from "../constants";
export type Auth = typeof auth.$inferSelect;
export const createAdmin = async (input: typeof apiCreateAdmin._type) => {
return await db.transaction(async (tx) => {
const hashedPassword = bcrypt.hashSync(input.password, 10);
const newAuth = await tx
.insert(auth)
.values({
email: input.email.toLowerCase(),
password: hashedPassword,
rol: "admin",
})
.returning()
.then((res) => res[0]);
if (!newAuth) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the user",
});
}
await tx
.insert(admins)
.values({
authId: newAuth.id,
...(!IS_CLOUD && {
serverIp:
process.env.ADVERTISE_ADDR || (await getPublicIpWithFallback()),
}),
})
.returning();
return newAuth;
});
};
export const createUser = async (input: typeof apiCreateUser._type) => {
return await db.transaction(async (tx) => {
const hashedPassword = bcrypt.hashSync(input.password, 10);
const res = await tx
.update(auth)
.set({
password: hashedPassword,
})
.where(eq(auth.id, input.id))
.returning()
.then((res) => res[0]);
if (!res) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the user",
});
}
const user = await tx
.update(users)
.set({
isRegistered: true,
expirationDate: undefined,
})
.where(eq(users.token, input.token))
.returning()
.then((res) => res[0]);
return user;
});
};
export const findAuthByEmail = async (email: string) => {
const result = await db.query.auth.findFirst({
where: eq(auth.email, email),
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
return result;
};
export const findAuthById = async (authId: string) => {
const result = await db.query.auth.findFirst({
where: eq(auth.id, authId),
columns: {
password: false,
},
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Auth not found",
});
}
return result;
};
export const updateAuthById = async (
authId: string,
authData: Partial<Auth>,
) => {
const result = await db
.update(auth)
.set({
...authData,
})
.where(eq(auth.id, authId))
.returning();
return result[0];
};
export const generate2FASecret = async (authId: string) => {
const auth = await findAuthById(authId);
const base32_secret = generateBase32Secret();
const totp = new TOTP({
issuer: "Dokploy",
label: `${auth?.email}`,
algorithm: "SHA1",
digits: 6,
secret: base32_secret,
});
const otpauth_url = totp.toString();
const qrUrl = await QRCode.toDataURL(otpauth_url);
return {
qrCodeUrl: qrUrl,
secret: base32_secret,
};
};
export const verify2FA = async (
auth: Omit<Auth, "password">,
secret: string,
pin: string,
) => {
const totp = new TOTP({
issuer: "Dokploy",
label: `${auth?.email}`,
algorithm: "SHA1",
digits: 6,
secret: secret,
});
const delta = totp.validate({ token: pin });
if (delta === null) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid 2FA code",
});
}
return auth;
};
const generateBase32Secret = () => {
const buffer = randomBytes(15);
const base32 = encode.encode(buffer).replace(/=/g, "").substring(0, 24);
return base32;
};

View File

@@ -2,8 +2,6 @@ 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 { IS_CLOUD } from "../constants";
import { removeScheduleBackup, scheduleBackup } from "../utils/backups/utils";
export type Backup = typeof backups.$inferSelect;

View File

@@ -12,14 +12,14 @@ export type Bitbucket = typeof bitbucket.$inferSelect;
export const createBitbucket = async (
input: typeof apiCreateBitbucket._type,
adminId: string,
organizationId: string,
) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
.insert(gitProvider)
.values({
providerType: "bitbucket",
adminId: adminId,
organizationId: organizationId,
name: input.name,
})
.returning()
@@ -74,12 +74,12 @@ export const updateBitbucket = async (
.where(eq(bitbucket.bitbucketId, bitbucketId))
.returning();
if (input.name || input.adminId) {
if (input.name || input.organizationId) {
await tx
.update(gitProvider)
.set({
name: input.name,
adminId: input.adminId,
organizationId: input.organizationId,
})
.where(eq(gitProvider.gitProviderId, input.gitProviderId))
.returning();

View File

@@ -33,13 +33,13 @@ export const findCertificateById = async (certificateId: string) => {
export const createCertificate = async (
certificateData: z.infer<typeof apiCreateCertificate>,
adminId: string,
organizationId: string,
) => {
const certificate = await db
.insert(certificates)
.values({
...certificateData,
adminId: adminId,
organizationId: organizationId,
})
.returning();

View File

@@ -44,10 +44,9 @@ import {
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { findAdminById, getDokployUrl } from "./admin";
import { getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
import { validUniqueServerAppName } from "./project";
import { cleanupFullDocker } from "./settings";
export type Compose = typeof compose.$inferSelect;
@@ -217,10 +216,10 @@ export const deployCompose = async ({
});
try {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
if (compose.sourceType === "github") {
await cloneGithubRepository({
...compose,
@@ -247,7 +246,7 @@ export const deployCompose = async ({
applicationName: compose.name,
applicationType: "compose",
buildLink,
adminId: compose.project.adminId,
organizationId: compose.project.organizationId,
domains: compose.domains,
});
} catch (error) {
@@ -262,7 +261,7 @@ export const deployCompose = async ({
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
adminId: compose.project.adminId,
organizationId: compose.project.organizationId,
});
throw error;
}
@@ -286,10 +285,10 @@ export const rebuildCompose = async ({
});
try {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
if (compose.serverId) {
await getBuildComposeCommand(compose, deployment.logPath);
} else {
@@ -332,10 +331,10 @@ export const deployRemoteCompose = async ({
});
try {
if (compose.serverId) {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
let command = "set -e;";
if (compose.sourceType === "github") {
@@ -381,7 +380,7 @@ export const deployRemoteCompose = async ({
applicationName: compose.name,
applicationType: "compose",
buildLink,
adminId: compose.project.adminId,
organizationId: compose.project.organizationId,
domains: compose.domains,
});
} catch (error) {
@@ -406,7 +405,7 @@ export const deployRemoteCompose = async ({
// @ts-ignore
errorMessage: error?.message || "Error building",
buildLink,
adminId: compose.project.adminId,
organizationId: compose.project.organizationId,
});
throw error;
}
@@ -430,10 +429,10 @@ export const rebuildRemoteCompose = async ({
});
try {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
if (compose.serverId) {
await getBuildComposeCommand(compose, deployment.logPath);
}

View File

@@ -12,7 +12,7 @@ import {
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { format } from "date-fns";
import { and, desc, eq, isNull } from "drizzle-orm";
import { desc, eq } from "drizzle-orm";
import {
type Application,
findApplicationById,
@@ -278,9 +278,11 @@ export const removeDeployment = async (deploymentId: string) => {
.returning();
return deployment[0];
} catch (error) {
const message =
error instanceof Error ? error.message : "Error creating the deployment";
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error deleting this deployment",
message,
});
}
};
@@ -304,7 +306,7 @@ const removeLastTenDeployments = async (
});
if (deploymentList.length > 10) {
const deploymentsToDelete = deploymentList.slice(10);
const deploymentsToDelete = deploymentList.slice(9);
if (serverId) {
let command = "";
for (const oldDeployment of deploymentsToDelete) {
@@ -340,7 +342,7 @@ const removeLastTenComposeDeployments = async (
if (deploymentList.length > 10) {
if (serverId) {
let command = "";
const deploymentsToDelete = deploymentList.slice(10);
const deploymentsToDelete = deploymentList.slice(9);
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
@@ -352,7 +354,7 @@ const removeLastTenComposeDeployments = async (
await execAsyncRemote(serverId, command);
} else {
const deploymentsToDelete = deploymentList.slice(10);
const deploymentsToDelete = deploymentList.slice(9);
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
if (existsSync(logPath)) {
@@ -374,7 +376,7 @@ export const removeLastTenPreviewDeploymenById = async (
});
if (deploymentList.length > 10) {
const deploymentsToDelete = deploymentList.slice(10);
const deploymentsToDelete = deploymentList.slice(9);
if (serverId) {
let command = "";
for (const oldDeployment of deploymentsToDelete) {
@@ -535,9 +537,11 @@ export const createServerDeployment = async (
}
return deploymentCreate[0];
} catch (error) {
const message =
error instanceof Error ? error.message : "Error creating the deployment";
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the deployment",
message,
});
}
};

View File

@@ -10,13 +10,13 @@ export type Destination = typeof destinations.$inferSelect;
export const createDestintation = async (
input: typeof apiCreateDestination._type,
adminId: string,
organizationId: string,
) => {
const newDestination = await db
.insert(destinations)
.values({
...input,
adminId: adminId,
organizationId: organizationId,
})
.returning()
.then((value) => value[0]);
@@ -46,14 +46,14 @@ export const findDestinationById = async (destinationId: string) => {
export const removeDestinationById = async (
destinationId: string,
adminId: string,
organizationId: string,
) => {
const result = await db
.delete(destinations)
.where(
and(
eq(destinations.destinationId, destinationId),
eq(destinations.adminId, adminId),
eq(destinations.organizationId, organizationId),
),
)
.returning();
@@ -73,7 +73,7 @@ export const updateDestinationById = async (
.where(
and(
eq(destinations.destinationId, destinationId),
eq(destinations.adminId, destinationData.adminId || ""),
eq(destinations.organizationId, destinationData.organizationId || ""),
),
)
.returning();

View File

@@ -98,7 +98,7 @@ export const getConfig = async (
const config = JSON.parse(stdout);
return config;
} catch (error) {}
} catch (_error) {}
};
export const getContainersByAppNameMatch = async (
@@ -136,27 +136,29 @@ export const getContainersByAppNameMatch = async (
result = stdout.trim().split("\n");
}
const containers = result.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const containers = result
.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
return {
containerId,
name,
state,
};
});
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
return {
containerId,
name,
state,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
return containers || [];
} catch (error) {}
} catch (_error) {}
return [];
};
@@ -190,31 +192,33 @@ export const getStackContainersByAppName = async (
result = stdout.trim().split("\n");
}
const containers = result.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const containers = result
.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state";
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
});
const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state";
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
return containers || [];
} catch (error) {}
} catch (_error) {}
return [];
};
@@ -249,45 +253,53 @@ export const getServiceContainersByAppName = async (
result = stdout.trim().split("\n");
}
const containers = result.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const containers = result
.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state";
const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state";
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
});
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
return containers || [];
} catch (error) {}
} catch (_error) {}
return [];
};
export const getContainersByAppLabel = async (
appName: string,
type: "standalone" | "swarm",
serverId?: string,
) => {
try {
let stdout = "";
let stderr = "";
const command = `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`;
const command =
type === "swarm"
? `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`
: type === "standalone"
? `docker ps --filter "name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`
: `docker ps --filter "label=com.docker.compose.project=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`;
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
@@ -306,26 +318,28 @@ export const getContainersByAppLabel = async (
const lines = stdout.trim().split("\n");
const containers = lines.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
return {
containerId,
name,
state,
};
});
const containers = lines
.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
return {
containerId,
name,
state,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
return containers || [];
} catch (error) {}
} catch (_error) {}
return [];
};
@@ -344,7 +358,7 @@ export const containerRestart = async (containerId: string) => {
const config = JSON.parse(stdout);
return config;
} catch (error) {}
} catch (_error) {}
};
export const getSwarmNodes = async (serverId?: string) => {
@@ -373,7 +387,7 @@ export const getSwarmNodes = async (serverId?: string) => {
.split("\n")
.map((line) => JSON.parse(line));
return nodesArray;
} catch (error) {}
} catch (_error) {}
};
export const getNodeInfo = async (nodeId: string, serverId?: string) => {
@@ -399,7 +413,7 @@ export const getNodeInfo = async (nodeId: string, serverId?: string) => {
const nodeInfo = JSON.parse(stdout);
return nodeInfo;
} catch (error) {}
} catch (_error) {}
};
export const getNodeApplications = async (serverId?: string) => {
@@ -431,7 +445,7 @@ export const getNodeApplications = async (serverId?: string) => {
.filter((service) => !service.Name.startsWith("dokploy-"));
return appArray;
} catch (error) {}
} catch (_error) {}
};
export const getApplicationInfo = async (
@@ -464,5 +478,5 @@ export const getApplicationInfo = async (
.map((line) => JSON.parse(line));
return appArray;
} catch (error) {}
} catch (_error) {}
};

View File

@@ -4,7 +4,7 @@ import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema";
import { findAdmin, findAdminById } from "./admin";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { findServerById } from "./server";
@@ -40,7 +40,7 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
export const generateTraefikMeDomain = async (
appName: string,
adminId: string,
userId: string,
serverId?: string,
) => {
if (serverId) {
@@ -57,7 +57,7 @@ export const generateTraefikMeDomain = async (
projectName: appName,
});
}
const admin = await findAdminById(adminId);
const admin = await findUserById(userId);
return generateRandomDomain({
serverIp: admin?.serverIp || "",
projectName: appName,
@@ -126,7 +126,6 @@ export const updateDomainById = async (
export const removeDomainById = async (domainId: string) => {
await findDomainById(domainId);
// TODO: fix order
const result = await db
.delete(domains)
.where(eq(domains.domainId, domainId))

View File

@@ -12,14 +12,14 @@ import { updatePreviewDeployment } from "./preview-deployment";
export type Github = typeof github.$inferSelect;
export const createGithub = async (
input: typeof apiCreateGithub._type,
adminId: string,
organizationId: string,
) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
.insert(gitProvider)
.values({
providerType: "github",
adminId: adminId,
organizationId: organizationId,
name: input.name,
})
.returning()
@@ -119,7 +119,7 @@ export const issueCommentExists = async ({
comment_id: comment_id,
});
return true;
} catch (error) {
} catch (_error) {
return false;
}
};

View File

@@ -1,9 +1,7 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateGitlab,
type bitbucket,
gitProvider,
type github,
gitlab,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
@@ -13,14 +11,14 @@ export type Gitlab = typeof gitlab.$inferSelect;
export const createGitlab = async (
input: typeof apiCreateGitlab._type,
adminId: string,
organizationId: string,
) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
.insert(gitProvider)
.values({
providerType: "gitlab",
adminId: adminId,
organizationId: organizationId,
name: input.name,
})
.returning()

View File

@@ -4,7 +4,7 @@ import {
backups,
mariadb,
} from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
import { pullImage } from "@dokploy/server/utils/docker/utils";

View File

@@ -1,6 +1,6 @@
import { db } from "@dokploy/server/db";
import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
import { pullImage } from "@dokploy/server/utils/docker/utils";

View File

@@ -123,8 +123,8 @@ export const updateMount = async (
mountId: string,
mountData: Partial<Mount>,
) => {
return await db.transaction(async (transaction) => {
const mount = await db
return await db.transaction(async (tx) => {
const mount = await tx
.update(mounts)
.set({
...mountData,
@@ -211,7 +211,7 @@ export const deleteFileMount = async (mountId: string) => {
} else {
await removeFileOrDirectory(fullPath);
}
} catch (error) {}
} catch (_error) {}
};
export const getBaseFilesPath = async (mountId: string) => {

View File

@@ -24,7 +24,7 @@ export type Notification = typeof notifications.$inferSelect;
export const createSlackNotification = async (
input: typeof apiCreateSlack._type,
adminId: string,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newSlack = await tx
@@ -54,7 +54,7 @@ export const createSlackNotification = async (
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "slack",
adminId: adminId,
organizationId: organizationId,
serverThreshold: input.serverThreshold,
})
.returning()
@@ -84,7 +84,7 @@ export const updateSlackNotification = async (
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
adminId: input.adminId,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
@@ -114,7 +114,7 @@ export const updateSlackNotification = async (
export const createTelegramNotification = async (
input: typeof apiCreateTelegram._type,
adminId: string,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newTelegram = await tx
@@ -122,6 +122,7 @@ export const createTelegramNotification = async (
.values({
botToken: input.botToken,
chatId: input.chatId,
messageThreadId: input.messageThreadId,
})
.returning()
.then((value) => value[0]);
@@ -144,7 +145,7 @@ export const createTelegramNotification = async (
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "telegram",
adminId: adminId,
organizationId: organizationId,
serverThreshold: input.serverThreshold,
})
.returning()
@@ -174,7 +175,7 @@ export const updateTelegramNotification = async (
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
adminId: input.adminId,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
@@ -193,6 +194,7 @@ export const updateTelegramNotification = async (
.set({
botToken: input.botToken,
chatId: input.chatId,
messageThreadId: input.messageThreadId,
})
.where(eq(telegram.telegramId, input.telegramId))
.returning()
@@ -204,7 +206,7 @@ export const updateTelegramNotification = async (
export const createDiscordNotification = async (
input: typeof apiCreateDiscord._type,
adminId: string,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newDiscord = await tx
@@ -234,7 +236,7 @@ export const createDiscordNotification = async (
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "discord",
adminId: adminId,
organizationId: organizationId,
serverThreshold: input.serverThreshold,
})
.returning()
@@ -264,7 +266,7 @@ export const updateDiscordNotification = async (
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
adminId: input.adminId,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
@@ -294,7 +296,7 @@ export const updateDiscordNotification = async (
export const createEmailNotification = async (
input: typeof apiCreateEmail._type,
adminId: string,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newEmail = await tx
@@ -328,7 +330,7 @@ export const createEmailNotification = async (
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "email",
adminId: adminId,
organizationId: organizationId,
serverThreshold: input.serverThreshold,
})
.returning()
@@ -358,7 +360,7 @@ export const updateEmailNotification = async (
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
adminId: input.adminId,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
@@ -392,7 +394,7 @@ export const updateEmailNotification = async (
export const createGotifyNotification = async (
input: typeof apiCreateGotify._type,
adminId: string,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newGotify = await tx
@@ -424,7 +426,7 @@ export const createGotifyNotification = async (
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "gotify",
adminId: adminId,
organizationId: organizationId,
})
.returning()
.then((value) => value[0]);
@@ -453,7 +455,7 @@ export const updateGotifyNotification = async (
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
adminId: input.adminId,
organizationId: input.organizationId,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()

View File

@@ -4,7 +4,7 @@ import {
backups,
postgres,
} from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
import { pullImage } from "@dokploy/server/utils/docker/utils";

View File

@@ -2,23 +2,20 @@ import { db } from "@dokploy/server/db";
import {
type apiCreatePreviewDeployment,
deployments,
organization,
previewDeployments,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { and, desc, eq } from "drizzle-orm";
import { slugify } from "../setup/server-setup";
import { generatePassword, generateRandomDomain } from "../templates/utils";
import { generatePassword } from "../templates/utils";
import { removeService } from "../utils/docker/utils";
import { removeDirectoryCode } from "../utils/filesystem/directory";
import { authGithub } from "../utils/providers/github";
import { removeTraefikConfig } from "../utils/traefik/application";
import { manageDomain } from "../utils/traefik/domain";
import { findAdminById } from "./admin";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import {
removeDeployments,
removeDeploymentsByPreviewDeploymentId,
} from "./deployment";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github";
@@ -106,13 +103,17 @@ export const removePreviewDeployment = async (previewDeploymentId: string) => {
for (const operation of cleanupOperations) {
try {
await operation();
} catch (error) {}
} catch (_error) {}
}
return deployment[0];
} catch (error) {
const message =
error instanceof Error
? error.message
: "Error deleting this preview deployment";
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error deleting this preview deployment",
message,
});
}
};
@@ -154,11 +155,14 @@ export const createPreviewDeployment = async (
const application = await findApplicationById(schema.applicationId);
const appName = `preview-${application.appName}-${generatePassword(6)}`;
const org = await db.query.organization.findFirst({
where: eq(organization.id, application.project.organizationId),
});
const generateDomain = await generateWildcardDomain(
application.previewWildcard || "*.traefik.me",
appName,
application.server?.ipAddress || "",
application.project.adminId,
org?.ownerId || "",
);
const octokit = authGithub(application?.github as Github);
@@ -199,6 +203,7 @@ export const createPreviewDeployment = async (
port: application.previewPort,
https: application.previewHttps,
certificateType: application.previewCertificateType,
customCertResolver: application.previewCustomCertResolver,
domainType: "preview",
previewDeploymentId: previewDeployment.previewDeploymentId,
});
@@ -250,7 +255,7 @@ const generateWildcardDomain = async (
baseDomain: string,
appName: string,
serverIp: string,
adminId: string,
userId: string,
): Promise<string> => {
if (!baseDomain.startsWith("*.")) {
throw new Error('The base domain must start with "*."');
@@ -268,7 +273,7 @@ const generateWildcardDomain = async (
}
if (!ip) {
const admin = await findAdminById(adminId);
const admin = await findUserById(userId);
ip = admin?.serverIp || "";
}

View File

@@ -16,13 +16,13 @@ export type Project = typeof projects.$inferSelect;
export const createProject = async (
input: typeof apiCreateProject._type,
adminId: string,
organizationId: string,
) => {
const newProject = await db
.insert(projects)
.values({
...input,
adminId: adminId,
organizationId: organizationId,
})
.returning()
.then((value) => value[0]);

View File

@@ -6,7 +6,7 @@ import {
updateRedirectMiddleware,
} from "@dokploy/server/utils/traefik/redirect";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { eq } from "drizzle-orm";
import type { z } from "zod";
import { findApplicationById } from "./application";
export type Redirect = typeof redirects.$inferSelect;
@@ -114,9 +114,11 @@ export const updateRedirectById = async (
return redirect;
} catch (error) {
const message =
error instanceof Error ? error.message : "Error updating this redirect";
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error updating this redirect",
message,
});
}
};

View File

@@ -1,6 +1,6 @@
import { db } from "@dokploy/server/db";
import { type apiCreateRedis, redis } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { buildAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { buildRedis } from "@dokploy/server/utils/databases/redis";
import { pullImage } from "@dokploy/server/utils/docker/utils";

View File

@@ -12,14 +12,14 @@ export type Registry = typeof registry.$inferSelect;
export const createRegistry = async (
input: typeof apiCreateRegistry._type,
adminId: string,
organizationId: string,
) => {
return await db.transaction(async (tx) => {
const newRegistry = await tx
.insert(registry)
.values({
...input,
adminId: adminId,
organizationId: organizationId,
})
.returning()
.then((value) => value[0]);
@@ -112,9 +112,11 @@ export const updateRegistry = async (
return response;
} catch (error) {
const message =
error instanceof Error ? error.message : "Error updating this registry";
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error updating this registry",
message,
});
}
};
@@ -135,9 +137,11 @@ export const findRegistryById = async (registryId: string) => {
return registryResponse;
};
export const findAllRegistryByAdminId = async (adminId: string) => {
export const findAllRegistryByOrganizationId = async (
organizationId: string,
) => {
const registryResponse = await db.query.registry.findMany({
where: eq(registry.adminId, adminId),
where: eq(registry.organizationId, organizationId),
});
return registryResponse;
};

View File

@@ -76,9 +76,11 @@ export const deleteSecurityById = async (securityId: string) => {
await removeSecurityMiddleware(application, result);
return result;
} catch (error) {
const message =
error instanceof Error ? error.message : "Error removing this security";
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error removing this security",
message,
});
}
};
@@ -98,9 +100,11 @@ export const updateSecurityById = async (
return response[0];
} catch (error) {
const message =
error instanceof Error ? error.message : "Error updating this security";
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error updating this security",
message,
});
}
};

View File

@@ -1,19 +1,24 @@
import { db } from "@dokploy/server/db";
import { type apiCreateServer, server } from "@dokploy/server/db/schema";
import {
type apiCreateServer,
organization,
server,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { eq } from "drizzle-orm";
export type Server = typeof server.$inferSelect;
export const createServer = async (
input: typeof apiCreateServer._type,
adminId: string,
organizationId: string,
) => {
const newServer = await db
.insert(server)
.values({
...input,
adminId: adminId,
organizationId: organizationId,
createdAt: new Date().toISOString(),
})
.returning()
.then((value) => value[0]);
@@ -45,12 +50,16 @@ export const findServerById = async (serverId: string) => {
return currentServer;
};
export const findServersByAdminId = async (adminId: string) => {
const servers = await db.query.server.findMany({
where: eq(server.adminId, adminId),
orderBy: desc(server.createdAt),
export const findServersByUserId = async (userId: string) => {
const orgs = await db.query.organization.findMany({
where: eq(organization.ownerId, userId),
with: {
servers: true,
},
});
const servers = orgs.flatMap((org) => org.servers);
return servers;
};

View File

@@ -5,8 +5,6 @@ import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { findAdminById } from "./admin";
// import packageInfo from "../../../package.json";
export interface IUpdateData {
latestVersion: string | null;
@@ -171,7 +169,6 @@ echo "$json_output"
const result = JSON.parse(stdout);
return result;
}
const items = readdirSync(dirPath, { withFileTypes: true });
const stack = [dirPath];
const result: TreeDataItem[] = [];

View File

@@ -1,80 +1,53 @@
import { db } from "@dokploy/server/db";
import { users } from "@dokploy/server/db/schema";
import { apikey, member, users_temp } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { auth } from "../lib/auth";
export type User = typeof users.$inferSelect;
export type User = typeof users_temp.$inferSelect;
export const findUserById = async (userId: string) => {
const user = await db.query.users.findFirst({
where: eq(users.userId, userId),
});
if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
return user;
};
export const findUserByAuthId = async (authId: string) => {
const user = await db.query.users.findFirst({
where: eq(users.authId, authId),
with: {
auth: true,
},
});
if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
return user;
};
export const findUsers = async (adminId: string) => {
const currentUsers = await db.query.users.findMany({
where: eq(users.adminId, adminId),
with: {
auth: {
columns: {
secret: false,
},
},
},
});
return currentUsers;
};
export const addNewProject = async (authId: string, projectId: string) => {
const user = await findUserByAuthId(authId);
export const addNewProject = async (
userId: string,
projectId: string,
organizationId: string,
) => {
const userR = await findMemberById(userId, organizationId);
await db
.update(users)
.update(member)
.set({
accessedProjects: [...user.accessedProjects, projectId],
accessedProjects: [...userR.accessedProjects, projectId],
})
.where(eq(users.authId, authId));
.where(
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
);
};
export const addNewService = async (authId: string, serviceId: string) => {
const user = await findUserByAuthId(authId);
export const addNewService = async (
userId: string,
serviceId: string,
organizationId: string,
) => {
const userR = await findMemberById(userId, organizationId);
await db
.update(users)
.update(member)
.set({
accessedServices: [...user.accessedServices, serviceId],
accessedServices: [...userR.accessedServices, serviceId],
})
.where(eq(users.authId, authId));
.where(
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
);
};
export const canPerformCreationService = async (
userId: string,
projectId: string,
organizationId: string,
) => {
const { accessedProjects, canCreateServices } =
await findUserByAuthId(userId);
const { accessedProjects, canCreateServices } = await findMemberById(
userId,
organizationId,
);
const haveAccessToProject = accessedProjects.includes(projectId);
if (canCreateServices && haveAccessToProject) {
@@ -87,8 +60,9 @@ export const canPerformCreationService = async (
export const canPerformAccessService = async (
userId: string,
serviceId: string,
organizationId: string,
) => {
const { accessedServices } = await findUserByAuthId(userId);
const { accessedServices } = await findMemberById(userId, organizationId);
const haveAccessToService = accessedServices.includes(serviceId);
if (haveAccessToService) {
@@ -99,11 +73,14 @@ export const canPerformAccessService = async (
};
export const canPeformDeleteService = async (
authId: string,
userId: string,
serviceId: string,
organizationId: string,
) => {
const { accessedServices, canDeleteServices } =
await findUserByAuthId(authId);
const { accessedServices, canDeleteServices } = await findMemberById(
userId,
organizationId,
);
const haveAccessToService = accessedServices.includes(serviceId);
if (canDeleteServices && haveAccessToService) {
@@ -113,8 +90,11 @@ export const canPeformDeleteService = async (
return false;
};
export const canPerformCreationProject = async (authId: string) => {
const { canCreateProjects } = await findUserByAuthId(authId);
export const canPerformCreationProject = async (
userId: string,
organizationId: string,
) => {
const { canCreateProjects } = await findMemberById(userId, organizationId);
if (canCreateProjects) {
return true;
@@ -123,8 +103,11 @@ export const canPerformCreationProject = async (authId: string) => {
return false;
};
export const canPerformDeleteProject = async (authId: string) => {
const { canDeleteProjects } = await findUserByAuthId(authId);
export const canPerformDeleteProject = async (
userId: string,
organizationId: string,
) => {
const { canDeleteProjects } = await findMemberById(userId, organizationId);
if (canDeleteProjects) {
return true;
@@ -134,10 +117,11 @@ export const canPerformDeleteProject = async (authId: string) => {
};
export const canPerformAccessProject = async (
authId: string,
userId: string,
projectId: string,
organizationId: string,
) => {
const { accessedProjects } = await findUserByAuthId(authId);
const { accessedProjects } = await findMemberById(userId, organizationId);
const haveAccessToProject = accessedProjects.includes(projectId);
@@ -147,26 +131,45 @@ export const canPerformAccessProject = async (
return false;
};
export const canAccessToTraefikFiles = async (authId: string) => {
const { canAccessToTraefikFiles } = await findUserByAuthId(authId);
export const canAccessToTraefikFiles = async (
userId: string,
organizationId: string,
) => {
const { canAccessToTraefikFiles } = await findMemberById(
userId,
organizationId,
);
return canAccessToTraefikFiles;
};
export const checkServiceAccess = async (
authId: string,
userId: string,
serviceId: string,
organizationId: string,
action = "access" as "access" | "create" | "delete",
) => {
let hasPermission = false;
switch (action) {
case "create":
hasPermission = await canPerformCreationService(authId, serviceId);
hasPermission = await canPerformCreationService(
userId,
serviceId,
organizationId,
);
break;
case "access":
hasPermission = await canPerformAccessService(authId, serviceId);
hasPermission = await canPerformAccessService(
userId,
serviceId,
organizationId,
);
break;
case "delete":
hasPermission = await canPeformDeleteService(authId, serviceId);
hasPermission = await canPeformDeleteService(
userId,
serviceId,
organizationId,
);
break;
default:
hasPermission = false;
@@ -182,6 +185,7 @@ export const checkServiceAccess = async (
export const checkProjectAccess = async (
authId: string,
action: "create" | "delete" | "access",
organizationId: string,
projectId?: string,
) => {
let hasPermission = false;
@@ -190,13 +194,14 @@ export const checkProjectAccess = async (
hasPermission = await canPerformAccessProject(
authId,
projectId as string,
organizationId,
);
break;
case "create":
hasPermission = await canPerformCreationProject(authId);
hasPermission = await canPerformCreationProject(authId, organizationId);
break;
case "delete":
hasPermission = await canPerformDeleteProject(authId);
hasPermission = await canPerformDeleteProject(authId, organizationId);
break;
default:
hasPermission = false;
@@ -208,3 +213,82 @@ export const checkProjectAccess = async (
});
}
};
export const findMemberById = async (
userId: string,
organizationId: string,
) => {
const result = await db.query.member.findFirst({
where: and(
eq(member.userId, userId),
eq(member.organizationId, organizationId),
),
with: {
user: true,
},
});
if (!result) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Permission denied",
});
}
return result;
};
export const updateUser = async (userId: string, userData: Partial<User>) => {
const user = await db
.update(users_temp)
.set({
...userData,
})
.where(eq(users_temp.id, userId))
.returning()
.then((res) => res[0]);
return user;
};
export const createApiKey = async (
userId: string,
input: {
name: string;
prefix?: string;
expiresIn?: number;
metadata: {
organizationId: string;
};
rateLimitEnabled?: boolean;
rateLimitTimeWindow?: number;
rateLimitMax?: number;
remaining?: number;
refillAmount?: number;
refillInterval?: number;
},
) => {
const apiKey = await auth.createApiKey({
body: {
name: input.name,
expiresIn: input.expiresIn,
prefix: input.prefix,
rateLimitEnabled: input.rateLimitEnabled,
rateLimitTimeWindow: input.rateLimitTimeWindow,
rateLimitMax: input.rateLimitMax,
remaining: input.remaining,
refillAmount: input.refillAmount,
refillInterval: input.refillInterval,
userId,
},
});
if (input.metadata) {
await db
.update(apikey)
.set({
metadata: JSON.stringify(input.metadata),
})
.where(eq(apikey.id, apiKey.id));
}
return apiKey;
};

View File

@@ -1,7 +1,7 @@
import { findServerById } from "@dokploy/server/services/server";
import type { ContainerCreateOptions } from "dockerode";
import { IS_CLOUD } from "../constants";
import { findAdminById } from "../services/admin";
import { findUserById } from "../services/admin";
import { getDokployImageTag } from "../services/settings";
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
@@ -66,7 +66,7 @@ export const setupMonitoring = async (serverId: string) => {
await container.inspect();
await container.remove({ force: true });
console.log("Removed existing container");
} catch (error) {
} catch (_error) {
// Container doesn't exist, continue
}
@@ -80,8 +80,8 @@ export const setupMonitoring = async (serverId: string) => {
}
};
export const setupWebMonitoring = async (adminId: string) => {
const admin = await findAdminById(adminId);
export const setupWebMonitoring = async (userId: string) => {
const user = await findUserById(userId);
const containerName = "dokploy-monitoring";
let imageName = "dokploy/monitoring:latest";
@@ -96,7 +96,7 @@ export const setupWebMonitoring = async (adminId: string) => {
const settings: ContainerCreateOptions = {
name: containerName,
Env: [`METRICS_CONFIG=${JSON.stringify(admin?.metricsConfig)}`],
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
Image: imageName,
HostConfig: {
// Memory: 100 * 1024 * 1024, // 100MB en bytes
@@ -104,9 +104,9 @@ export const setupWebMonitoring = async (adminId: string) => {
// CapAdd: ["NET_ADMIN", "SYS_ADMIN"],
// Privileged: true,
PortBindings: {
[`${admin.metricsConfig.server.port}/tcp`]: [
[`${user?.metricsConfig?.server?.port}/tcp`]: [
{
HostPort: admin.metricsConfig.server.port.toString(),
HostPort: user?.metricsConfig?.server?.port.toString(),
},
],
},
@@ -120,7 +120,7 @@ export const setupWebMonitoring = async (adminId: string) => {
// NetworkMode: "host",
},
ExposedPorts: {
[`${admin.metricsConfig.server.port}/tcp`]: {},
[`${user?.metricsConfig?.server?.port}/tcp`]: {},
},
};
const docker = await getRemoteDocker();
@@ -135,7 +135,7 @@ export const setupWebMonitoring = async (adminId: string) => {
await container.inspect();
await container.remove({ force: true });
console.log("Removed existing container");
} catch (error) {}
} catch (_error) {}
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);

View File

@@ -55,7 +55,7 @@ export const initializePostgres = async () => {
...settings,
});
console.log("Postgres Started ✅");
} catch (error) {
} catch (_) {
try {
await docker.createService(settings);
} catch (error: any) {

View File

@@ -52,7 +52,7 @@ export const initializeRedis = async () => {
...settings,
});
console.log("Redis Started ✅");
} catch (error) {
} catch (_) {
try {
await docker.createService(settings);
} catch (error: any) {

View File

@@ -89,7 +89,7 @@ export const serverAudit = async (serverId: string) => {
.on("data", (data: string) => {
output += data;
})
.stderr.on("data", (data) => {});
.stderr.on("data", (_data) => {});
});
})
.on("error", (err) => {

View File

@@ -9,6 +9,7 @@ import {
TRAEFIK_PORT,
TRAEFIK_SSL_PORT,
TRAEFIK_VERSION,
TRAEFIK_HTTP3_PORT,
getDefaultMiddlewares,
getDefaultServerTraefikConfig,
} from "@dokploy/server/setup/traefik-setup";
@@ -170,6 +171,9 @@ ${installNixpacks()}
echo -e "12. Installing Buildpacks"
${installBuildpacks()}
echo -e "13. Installing Railpack"
${installRailpack()}
`;
return bashCommand;
@@ -539,22 +543,28 @@ export const installRClone = () => `
export const createTraefikInstance = () => {
const command = `
# Check if dokpyloy-traefik exists
if docker service ls | grep -q 'dokploy-traefik'; then
if docker service inspect dokploy-traefik > /dev/null 2>&1; then
echo "Migrating Traefik to Standalone..."
docker service rm dokploy-traefik
sleep 7
echo "Traefik migrated to Standalone ✅"
fi
if docker inspect dokploy-traefik > /dev/null 2>&1; then
echo "Traefik already exists ✅"
else
# Create the dokploy-traefik service
# Create the dokploy-traefik container
TRAEFIK_VERSION=${TRAEFIK_VERSION}
docker service create \
docker run -d \
--name dokploy-traefik \
--replicas 1 \
--constraint 'node.role==manager' \
--network dokploy-network \
--mount type=bind,src=/etc/dokploy/traefik/traefik.yml,dst=/etc/traefik/traefik.yml \
--mount type=bind,src=/etc/dokploy/traefik/dynamic,dst=/etc/dokploy/traefik/dynamic \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
--label traefik.enable=true \
--publish mode=host,target=${TRAEFIK_SSL_PORT},published=${TRAEFIK_SSL_PORT} \
--publish mode=host,target=${TRAEFIK_PORT},published=${TRAEFIK_PORT} \
--restart unless-stopped \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
-v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
-v /var/run/docker.sock:/var/run/docker.sock \
-p ${TRAEFIK_SSL_PORT}:${TRAEFIK_SSL_PORT} \
-p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \
-p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
traefik:v$TRAEFIK_VERSION
echo "Traefik version $TRAEFIK_VERSION installed ✅"
fi
@@ -573,6 +583,16 @@ const installNixpacks = () => `
fi
`;
const installRailpack = () => `
if command_exists railpack; then
echo "Railpack already installed ✅"
else
export RAILPACK_VERSION=0.0.37
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅"
fi
`;
const installBuildpacks = () => `
SUFFIX=""
if [ "$SYS_ARCH" = "aarch64" ] || [ "$SYS_ARCH" = "arm64" ]; then

View File

@@ -38,6 +38,18 @@ export const validateNixpacks = () => `
fi
`;
export const validateRailpack = () => `
if command_exists railpack; then
version=$(railpack --version | awk '{print $3}')
if [ -n "$version" ]; then
echo "$version true"
else
echo "0.0.0 false"
fi
else
echo "0.0.0 false"
fi
`;
export const validateBuildpacks = () => `
if command_exists pack; then
version=$(pack --version | awk '{print $1}')
@@ -86,7 +98,7 @@ export const serverValidate = async (serverId: string) => {
rcloneVersionEnabled=$(${validateRClone()})
nixpacksVersionEnabled=$(${validateNixpacks()})
buildpacksVersionEnabled=$(${validateBuildpacks()})
railpackVersionEnabled=$(${validateRailpack()})
dockerVersion=$(echo $dockerVersionEnabled | awk '{print $1}')
dockerEnabled=$(echo $dockerVersionEnabled | awk '{print $2}')
@@ -96,6 +108,9 @@ export const serverValidate = async (serverId: string) => {
nixpacksVersion=$(echo $nixpacksVersionEnabled | awk '{print $1}')
nixpacksEnabled=$(echo $nixpacksVersionEnabled | awk '{print $2}')
railpackVersion=$(echo $railpackVersionEnabled | awk '{print $1}')
railpackEnabled=$(echo $railpackVersionEnabled | awk '{print $2}')
buildpacksVersion=$(echo $buildpacksVersionEnabled | awk '{print $1}')
buildpacksEnabled=$(echo $buildpacksVersionEnabled | awk '{print $2}')
@@ -103,7 +118,7 @@ export const serverValidate = async (serverId: string) => {
isSwarmInstalled=$(${validateSwarm()})
isMainDirectoryInstalled=$(${validateMainDirectory()})
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled}"
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"railpack\\": {\\"version\\": \\"$railpackVersion\\", \\"enabled\\": $railpackEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled}"
`;
client.exec(bashCommand, (err, stream) => {
if (err) {
@@ -128,7 +143,7 @@ export const serverValidate = async (serverId: string) => {
.on("data", (data: string) => {
output += data;
})
.stderr.on("data", (data) => {});
.stderr.on("data", (_data) => {});
});
})
.on("error", (err) => {

View File

@@ -18,7 +18,7 @@ export const dockerSwarmInitialized = async () => {
await docker.swarmInspect();
return true;
} catch (e) {
} catch (_e) {
return false;
}
};
@@ -41,7 +41,7 @@ export const dockerNetworkInitialized = async () => {
try {
await docker.getNetwork("dokploy-network").inspect();
return true;
} catch (e) {
} catch (_e) {
return false;
}
};

View File

@@ -1,9 +1,8 @@
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import type { ContainerTaskSpec, CreateServiceOptions } from "dockerode";
import type { ContainerCreateOptions } from "dockerode";
import { dump } from "js-yaml";
import { paths } from "../constants";
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import type { FileConfig } from "../utils/traefik/file-types";
import type { MainTraefikConfig } from "../utils/traefik/types";
@@ -12,6 +11,8 @@ export const TRAEFIK_SSL_PORT =
Number.parseInt(process.env.TRAEFIK_SSL_PORT!, 10) || 443;
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.1.2";
interface TraefikOptions {
@@ -23,6 +24,7 @@ interface TraefikOptions {
publishedPort: number;
publishMode?: "ingress" | "host";
}[];
force?: boolean;
}
export const initializeTraefik = async ({
@@ -30,113 +32,86 @@ export const initializeTraefik = async ({
env,
serverId,
additionalPorts = [],
force = false,
}: TraefikOptions = {}) => {
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
const imageName = `traefik:v${TRAEFIK_VERSION}`;
const containerName = "dokploy-traefik";
const settings: CreateServiceOptions = {
Name: containerName,
TaskTemplate: {
ContainerSpec: {
Image: imageName,
Env: env,
Mounts: [
{
Type: "bind",
Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`,
Target: "/etc/traefik/traefik.yml",
},
{
Type: "bind",
Source: DYNAMIC_TRAEFIK_PATH,
Target: "/etc/dokploy/traefik/dynamic",
},
{
Type: "bind",
Source: "/var/run/docker.sock",
Target: "/var/run/docker.sock",
},
],
},
Networks: [{ Target: "dokploy-network" }],
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Ports: [
{
TargetPort: 443,
PublishedPort: TRAEFIK_SSL_PORT,
PublishMode: "host",
},
{
TargetPort: 80,
PublishedPort: TRAEFIK_PORT,
PublishMode: "host",
},
...(enableDashboard
? [
{
TargetPort: 8080,
PublishedPort: 8080,
PublishMode: "host" as const,
},
]
: []),
...additionalPorts.map((port) => ({
TargetPort: port.targetPort,
PublishedPort: port.publishedPort,
PublishMode: port.publishMode || ("host" as const),
})),
],
},
const exposedPorts: Record<string, {}> = {
[`${TRAEFIK_PORT}/tcp`]: {},
[`${TRAEFIK_SSL_PORT}/tcp`]: {},
[`${TRAEFIK_HTTP3_PORT}/udp`]: {},
};
const portBindings: Record<string, Array<{ HostPort: string }>> = {
[`${TRAEFIK_PORT}/tcp`]: [{ HostPort: TRAEFIK_PORT.toString() }],
[`${TRAEFIK_SSL_PORT}/tcp`]: [{ HostPort: TRAEFIK_SSL_PORT.toString() }],
[`${TRAEFIK_HTTP3_PORT}/udp`]: [
{ HostPort: TRAEFIK_HTTP3_PORT.toString() },
],
};
if (enableDashboard) {
exposedPorts["8080/tcp"] = {};
portBindings["8080/tcp"] = [{ HostPort: "8080" }];
}
for (const port of additionalPorts) {
const portKey = `${port.targetPort}/tcp`;
exposedPorts[portKey] = {};
portBindings[portKey] = [{ HostPort: port.publishedPort.toString() }];
}
const settings: ContainerCreateOptions = {
name: containerName,
Image: imageName,
NetworkingConfig: {
EndpointsConfig: {
"dokploy-network": {},
},
},
ExposedPorts: exposedPorts,
HostConfig: {
RestartPolicy: {
Name: "always",
},
Binds: [
`${MAIN_TRAEFIK_PATH}/traefik.yml:/etc/traefik/traefik.yml`,
`${DYNAMIC_TRAEFIK_PATH}:/etc/dokploy/traefik/dynamic`,
"/var/run/docker.sock:/var/run/docker.sock",
],
PortBindings: portBindings,
},
Env: env,
};
const docker = await getRemoteDocker(serverId);
try {
if (serverId) {
await pullRemoteImage(imageName, serverId);
} else {
await pullImage(imageName);
}
const service = docker.getService(containerName);
const inspect = await service.inspect();
const existingEnv = inspect.Spec.TaskTemplate.ContainerSpec.Env || [];
const updatedEnv = !env ? existingEnv : env;
const updatedSettings = {
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ContainerSpec: {
...(settings?.TaskTemplate as ContainerTaskSpec).ContainerSpec,
Env: updatedEnv,
},
},
};
await service.update({
version: Number.parseInt(inspect.Version.Index),
...updatedSettings,
});
console.log("Traefik Started ✅");
} catch (error) {
try {
await docker.createService(settings);
} catch (error: any) {
if (error?.statusCode !== 409) {
throw error;
const service = docker.getService("dokploy-traefik");
await service?.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (_) {}
const container = docker.getContainer(containerName);
try {
const inspect = await container.inspect();
if (inspect.State.Status === "running" && !force) {
console.log("Traefik already running");
return;
}
console.log("Traefik service already exists, continuing...");
await container.remove({ force: true });
} catch (error) {
console.log(error);
}
console.log("Traefik Not Found: Starting ✅");
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
} catch (error) {
console.log(error);
}
};
@@ -212,6 +187,9 @@ export const getDefaultTraefikConfig = () => {
},
websecure: {
address: `:${TRAEFIK_SSL_PORT}`,
http3: {
advertisedPort: TRAEFIK_HTTP3_PORT,
},
...(process.env.NODE_ENV === "production" && {
http: {
tls: {
@@ -267,6 +245,9 @@ export const getDefaultServerTraefikConfig = () => {
},
websecure: {
address: `:${TRAEFIK_SSL_PORT}`,
http3: {
advertisedPort: TRAEFIK_HTTP3_PORT,
},
http: {
tls: {
certResolver: "letsencrypt",

View File

@@ -36,7 +36,7 @@ type AnyObj = Record<PropertyKey, unknown>;
type ZodObj<T extends AnyObj> = {
[key in keyof T]: z.ZodType<T[key]>;
};
const zObject = <T extends AnyObj>(arg: ZodObj<T>) => z.object(arg);
const _zObject = <T extends AnyObj>(arg: ZodObj<T>) => z.object(arg);
// const goodDogScheme = zObject<UserWithPosts>({
// // prueba: schema.selectDatabaseSchema,

View File

@@ -1,122 +1,77 @@
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import { updateAdmin } from "@dokploy/server/services/admin";
import { type RotatingFileStream, createStream } from "rotating-file-stream";
import { db } from "../../db";
import { paths } from "@dokploy/server/constants";
import { execAsync } from "../process/execAsync";
import { findAdmin } from "@dokploy/server/services/admin";
import { updateUser } from "@dokploy/server/services/user";
import { scheduleJob, scheduledJobs } from "node-schedule";
class LogRotationManager {
private static instance: LogRotationManager;
private stream: RotatingFileStream | null = null;
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";
private constructor() {
if (IS_CLOUD) {
return;
}
this.initialize().catch(console.error);
}
public static getInstance(): LogRotationManager {
if (!LogRotationManager.instance) {
LogRotationManager.instance = new LogRotationManager();
}
return LogRotationManager.instance;
}
private async initialize(): Promise<void> {
const isActive = await this.getStateFromDB();
if (isActive) {
await this.activateStream();
}
}
private async getStateFromDB(): Promise<boolean> {
const setting = await db.query.admins.findFirst({});
return setting?.enableLogRotation ?? false;
}
private async setStateInDB(active: boolean): Promise<void> {
const admin = await db.query.admins.findFirst({});
if (!admin) {
return;
}
await updateAdmin(admin?.authId, {
enableLogRotation: active,
});
}
private async activateStream(): Promise<void> {
export const startLogCleanup = async (
cronExpression = "0 0 * * *",
): Promise<boolean> => {
try {
const { DYNAMIC_TRAEFIK_PATH } = paths();
if (this.stream) {
await this.deactivateStream();
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
if (existingJob) {
existingJob.cancel();
}
this.stream = createStream("access.log", {
size: "100M",
interval: "1d",
path: DYNAMIC_TRAEFIK_PATH,
rotate: 6,
compress: "gzip",
});
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`,
);
this.stream.on("rotation", this.handleRotation.bind(this));
}
private async deactivateStream(): Promise<void> {
return new Promise<void>((resolve) => {
if (this.stream) {
this.stream.end(() => {
this.stream = null;
resolve();
});
} else {
resolve();
await execAsync("docker exec dokploy-traefik kill -USR1 1");
} catch (error) {
console.error("Error during log cleanup:", error);
}
});
}
public async activate(): Promise<boolean> {
const currentState = await this.getStateFromDB();
if (currentState) {
return true;
const admin = await findAdmin();
if (admin) {
await updateUser(admin.user.id, {
logCleanupCron: cronExpression,
});
}
await this.setStateInDB(true);
await this.activateStream();
return true;
} catch (_) {
return false;
}
};
public async deactivate(): Promise<boolean> {
console.log("Deactivating log rotation...");
const currentState = await this.getStateFromDB();
if (!currentState) {
console.log("Log rotation is already inactive in DB");
return true;
export const stopLogCleanup = async (): Promise<boolean> => {
try {
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
if (existingJob) {
existingJob.cancel();
}
// Update database
const admin = await findAdmin();
if (admin) {
await updateUser(admin.user.id, {
logCleanupCron: null,
});
}
await this.setStateInDB(false);
await this.deactivateStream();
console.log("Log rotation deactivated successfully");
return true;
} catch (error) {
console.error("Error stopping log cleanup:", error);
return false;
}
};
private async handleRotation() {
try {
const status = await this.getStatus();
if (!status) {
await this.deactivateStream();
}
await execAsync(
"docker kill -s USR1 $(docker ps -q --filter name=dokploy-traefik)",
);
console.log("USR1 Signal send to Traefik");
} catch (error) {
console.error("Error sending USR1 Signal to Traefik:", error);
}
}
public async getStatus(): Promise<boolean> {
const dbState = await this.getStateFromDB();
return dbState;
}
}
export const logRotationManager = LogRotationManager.getInstance();
export const getLogCleanupStatus = async (): Promise<{
enabled: boolean;
cronExpression: string | null;
}> => {
const admin = await findAdmin();
const cronExpression = admin?.user.logCleanupCron ?? null;
return {
enabled: cronExpression !== null,
cronExpression,
};
};

View File

@@ -6,14 +6,21 @@ interface HourlyData {
count: number;
}
export function processLogs(logString: string): HourlyData[] {
export function processLogs(
logString: string,
dateRange?: { start?: string; end?: string },
): HourlyData[] {
if (_.isEmpty(logString)) {
return [];
}
const hourlyData = _(logString)
.split("\n")
.compact()
.filter((line) => {
const trimmed = line.trim();
// Check if the line starts with { and ends with } to ensure it's a potential JSON object
return trimmed !== "" && trimmed.startsWith("{") && trimmed.endsWith("}");
})
.map((entry) => {
try {
const log: LogEntry = JSON.parse(entry);
@@ -21,6 +28,20 @@ export function processLogs(logString: string): HourlyData[] {
return null;
}
const date = new Date(log.StartUTC);
if (dateRange?.start || dateRange?.end) {
const logDate = date.getTime();
const start = dateRange?.start
? new Date(dateRange.start).getTime()
: 0;
const end = dateRange?.end
? new Date(dateRange.end).getTime()
: Number.POSITIVE_INFINITY;
if (logDate < start || logDate > end) {
return null;
}
}
return `${date.toISOString().slice(0, 13)}:00:00Z`;
} catch (error) {
console.error("Error parsing log entry:", error);
@@ -51,21 +72,46 @@ export function parseRawConfig(
sort?: SortInfo,
search?: string,
status?: string[],
dateRange?: { start?: string; end?: string },
): { data: LogEntry[]; totalCount: number } {
try {
if (_.isEmpty(rawConfig)) {
return { data: [], totalCount: 0 };
}
// Split logs into chunks to avoid memory issues
let parsedLogs = _(rawConfig)
.split("\n")
.filter((line) => {
const trimmed = line.trim();
return (
trimmed !== "" && trimmed.startsWith("{") && trimmed.endsWith("}")
);
})
.map((line) => {
try {
return JSON.parse(line) as LogEntry;
} catch (error) {
console.error("Error parsing log line:", error);
return null;
}
})
.compact()
.map((line) => JSON.parse(line) as LogEntry)
.value();
parsedLogs = parsedLogs.filter(
(log) => log.ServiceName !== "dokploy-service-app@file",
);
// Apply date range filter if provided
if (dateRange?.start || dateRange?.end) {
parsedLogs = parsedLogs.filter((log) => {
const logDate = new Date(log.StartUTC).getTime();
const start = dateRange?.start
? new Date(dateRange.start).getTime()
: 0;
const end = dateRange?.end
? new Date(dateRange.end).getTime()
: Number.POSITIVE_INFINITY;
return logDate >= start && logDate <= end;
});
}
if (search) {
parsedLogs = parsedLogs.filter((log) =>
@@ -78,6 +124,7 @@ export function parseRawConfig(
status.some((range) => isStatusInRange(log.DownstreamStatus, range)),
);
}
const totalCount = parsedLogs.length;
if (sort) {
@@ -101,6 +148,7 @@ export function parseRawConfig(
throw new Error("Failed to parse rawConfig");
}
}
const isStatusInRange = (status: number, range: string) => {
switch (range) {
case "info":

View File

@@ -0,0 +1 @@
export * from "./select-ai-provider";

View File

@@ -0,0 +1,112 @@
import { createAnthropic } from "@ai-sdk/anthropic";
import { createAzure } from "@ai-sdk/azure";
import { createCohere } from "@ai-sdk/cohere";
import { createDeepInfra } from "@ai-sdk/deepinfra";
import { createMistral } from "@ai-sdk/mistral";
import { createOpenAI } from "@ai-sdk/openai";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { createOllama } from "ollama-ai-provider";
function getProviderName(apiUrl: string) {
if (apiUrl.includes("api.openai.com")) return "openai";
if (apiUrl.includes("azure.com")) return "azure";
if (apiUrl.includes("api.anthropic.com")) return "anthropic";
if (apiUrl.includes("api.cohere.ai")) return "cohere";
if (apiUrl.includes("api.perplexity.ai")) return "perplexity";
if (apiUrl.includes("api.mistral.ai")) return "mistral";
if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
return "ollama";
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
return "custom";
}
export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
const providerName = getProviderName(config.apiUrl);
switch (providerName) {
case "openai":
return createOpenAI({
apiKey: config.apiKey,
baseURL: config.apiUrl,
});
case "azure":
return createAzure({
apiKey: config.apiKey,
baseURL: config.apiUrl,
});
case "anthropic":
return createAnthropic({
apiKey: config.apiKey,
baseURL: config.apiUrl,
});
case "cohere":
return createCohere({
baseURL: config.apiUrl,
apiKey: config.apiKey,
});
case "perplexity":
return createOpenAICompatible({
name: "perplexity",
baseURL: config.apiUrl,
headers: {
Authorization: `Bearer ${config.apiKey}`,
},
});
case "mistral":
return createMistral({
baseURL: config.apiUrl,
apiKey: config.apiKey,
});
case "ollama":
return createOllama({
// optional settings, e.g.
baseURL: config.apiUrl,
});
case "deepinfra":
return createDeepInfra({
baseURL: config.apiUrl,
apiKey: config.apiKey,
});
case "custom":
return createOpenAICompatible({
name: "custom",
baseURL: config.apiUrl,
headers: {
Authorization: `Bearer ${config.apiKey}`,
},
});
default:
throw new Error(`Unsupported AI provider: ${providerName}`);
}
}
export const getProviderHeaders = (
apiUrl: string,
apiKey: string,
): Record<string, string> => {
// Anthropic
if (apiUrl.includes("anthropic")) {
return {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
};
}
// Mistral
if (apiUrl.includes("mistral")) {
return {
Authorization: apiKey,
};
}
// Default (OpenAI style)
return {
Authorization: `Bearer ${apiKey}`,
};
};
export interface Model {
id: string;
object: string;
created: number;
owned_by: string;
}

View File

@@ -1,4 +1,4 @@
import { findAdmin } from "@dokploy/server/services/admin";
import path from "node:path";
import { getAllServers } from "@dokploy/server/services/server";
import { scheduleJob } from "node-schedule";
import { db } from "../../db/index";
@@ -12,13 +12,19 @@ import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres";
import { findAdmin } from "../../services/admin";
import { getS3Credentials } from "./utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { startLogCleanup } from "../access-log/handler";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");
const admin = await findAdmin();
if (admin?.enableDockerCleanup) {
if (admin?.user.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
@@ -26,7 +32,7 @@ export const initCronJobs = async () => {
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
await sendDockerCleanupNotifications(admin.adminId);
await sendDockerCleanupNotifications(admin.user.id);
});
}
@@ -43,7 +49,7 @@ export const initCronJobs = async () => {
await cleanUpDockerBuilder(serverId);
await cleanUpSystemPrune(serverId);
await sendDockerCleanupNotifications(
admin.adminId,
admin.user.id,
`Docker cleanup for Server ${name} (${serverId})`,
);
});
@@ -168,4 +174,44 @@ export const initCronJobs = async () => {
}
}
}
if (admin?.user.logCleanupCron) {
await startLogCleanup(admin.user.logCleanupCron);
}
};
export const keepLatestNBackups = async (
backup: BackupSchedule,
serverId?: string | null,
) => {
// 0 also immediately returns which is good as the empty "keep latest" field in the UI
// is saved as 0 in the database
if (!backup.keepLatestCount) return;
try {
const rcloneFlags = getS3Credentials(backup.destination);
const backupFilesPath = path.join(
`:s3:${backup.destination.bucket}`,
backup.prefix,
);
// --include "*.sql.gz" ensures nothing else other than the db backup files are touched by rclone
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*.sql.gz" ${backupFilesPath}`;
// when we pipe the above command with this one, we only get the list of files we want to delete
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
// this command deletes the files
// to test the deletion before actually deleting we can add --dry-run before ${backupFilesPath}/{}
const rcloneDelete = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}/{}`;
const rcloneCommand = `${rcloneList} | ${sortAndPickUnwantedBackups} ${rcloneDelete}`;
if (serverId) {
await execAsyncRemote(serverId, rcloneCommand);
} else {
await execAsync(rcloneCommand);
}
} catch (error) {
console.error(error);
throw error;
}
};

View File

@@ -49,7 +49,7 @@ export const runMariadbBackup = async (
projectName: project.name,
databaseType: "mariadb",
type: "success",
adminId: project.adminId,
organizationId: project.organizationId,
});
} catch (error) {
console.log(error);
@@ -60,7 +60,7 @@ export const runMariadbBackup = async (
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
adminId: project.adminId,
organizationId: project.organizationId,
});
throw error;
}

View File

@@ -46,7 +46,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
projectName: project.name,
databaseType: "mongodb",
type: "success",
adminId: project.adminId,
organizationId: project.organizationId,
});
} catch (error) {
console.log(error);
@@ -57,7 +57,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
adminId: project.adminId,
organizationId: project.organizationId,
});
throw error;
}

View File

@@ -1,4 +1,3 @@
import { unlink } from "node:fs/promises";
import path from "node:path";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { MySql } from "@dokploy/server/services/mysql";
@@ -46,7 +45,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
projectName: project.name,
databaseType: "mysql",
type: "success",
adminId: project.adminId,
organizationId: project.organizationId,
});
} catch (error) {
console.log(error);
@@ -57,7 +56,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
adminId: project.adminId,
organizationId: project.organizationId,
});
throw error;
}

View File

@@ -49,7 +49,7 @@ export const runPostgresBackup = async (
projectName: project.name,
databaseType: "postgres",
type: "success",
adminId: project.adminId,
organizationId: project.organizationId,
});
} catch (error) {
await sendDatabaseBackupNotifications({
@@ -59,7 +59,7 @@ export const runPostgresBackup = async (
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
adminId: project.adminId,
organizationId: project.organizationId,
});
throw error;

View File

@@ -5,6 +5,7 @@ import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres";
import { keepLatestNBackups } from ".";
export const scheduleBackup = (backup: BackupSchedule) => {
const { schedule, backupId, databaseType, postgres, mysql, mongo, mariadb } =
@@ -12,12 +13,16 @@ export const scheduleBackup = (backup: BackupSchedule) => {
scheduleJob(backupId, schedule, async () => {
if (databaseType === "postgres" && postgres) {
await runPostgresBackup(postgres, backup);
await keepLatestNBackups(backup, postgres.serverId);
} else if (databaseType === "mysql" && mysql) {
await runMySqlBackup(mysql, backup);
await keepLatestNBackups(backup, mysql.serverId);
} else if (databaseType === "mongo" && mongo) {
await runMongoBackup(mongo, backup);
await keepLatestNBackups(backup, mongo.serverId);
} else if (databaseType === "mariadb" && mariadb) {
await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, mariadb.serverId);
}
});
};
@@ -28,7 +33,7 @@ export const removeScheduleBackup = (backupId: string) => {
};
export const getS3Credentials = (destination: Destination) => {
const { accessKey, secretAccessKey, bucket, region, endpoint, provider } =
const { accessKey, secretAccessKey, region, endpoint, provider } =
destination;
const rcloneFlags = [
`--s3-access-key-id=${accessKey}`,

View File

@@ -2,7 +2,6 @@ import {
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
@@ -99,8 +98,7 @@ export const getBuildComposeCommand = async (
logPath: string,
) => {
const { COMPOSE_PATH } = paths(true);
const { sourceType, appName, mounts, composeType, domains, composePath } =
compose;
const { sourceType, appName, mounts, composeType, domains } = compose;
const command = createCommand(compose);
const envCommand = getCreateEnvFileCommand(compose);
const projectPath = join(COMPOSE_PATH, compose.appName, "code");

View File

@@ -16,6 +16,7 @@ import { buildCustomDocker, getDockerCommand } from "./docker-file";
import { buildHeroku, getHerokuCommand } from "./heroku";
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
import { buildPaketo, getPaketoCommand } from "./paketo";
import { buildRailpack, getRailpackCommand } from "./railpack";
import { buildStatic, getStaticCommand } from "./static";
// NIXPACKS codeDirectory = where is the path of the code directory
@@ -55,6 +56,8 @@ export const buildApplication = async (
await buildCustomDocker(application, writeStream);
} else if (buildType === "static") {
await buildStatic(application, writeStream);
} else if (buildType === "railpack") {
await buildRailpack(application, writeStream);
}
if (application.registryId) {
@@ -96,6 +99,9 @@ export const getBuildCommand = (
case "dockerfile":
command = getDockerCommand(application, logPath);
break;
case "railpack":
command = getRailpackCommand(application, logPath);
break;
}
if (registry) {
command += uploadImageRemoteCommand(application, logPath);
@@ -197,7 +203,7 @@ export const mechanizeDockerContainer = async (
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch (error) {
} catch (_error) {
await docker.createService(settings);
}
};

View File

@@ -91,7 +91,7 @@ export const getNixpacksCommand = (
application: ApplicationNested,
logPath: string,
) => {
const { env, appName, publishDirectory, serverId } = application;
const { env, appName, publishDirectory } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const buildContainerId = `${appName}-${nanoid(10)}`;

View File

@@ -0,0 +1,87 @@
import type { WriteStream } from "node:fs";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import { execAsync } from "../process/execAsync";
export const buildRailpack = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { env, appName } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
);
try {
// Ensure buildkit container is running, create if it doesn't exist
await execAsync(
"docker container inspect buildkit >/dev/null 2>&1 || docker run --rm --privileged -d --name buildkit moby/buildkit",
);
// Build the application using railpack
const args = ["build", buildAppDirectory, "--name", appName];
// Add environment variables
for (const env of envVariables) {
args.push("--env", env);
}
await spawnAsync(
"railpack",
args,
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
{
env: {
...process.env,
BUILDKIT_HOST: "docker-container://buildkit",
},
},
);
return true;
} catch (e) {
throw e;
}
};
export const getRailpackCommand = (
application: ApplicationNested,
logPath: string,
) => {
const { env, appName } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
);
// Build the application using railpack
const args = ["build", buildAppDirectory, "--name", appName];
// Add environment variables
for (const env of envVariables) {
args.push("--env", env);
}
const command = `railpack ${args.join(" ")}`;
const bashCommand = `
echo "Building with Railpack..." >> "${logPath}";
docker container inspect buildkit >/dev/null 2>&1 || docker run --rm --privileged -d --name buildkit moby/buildkit;
export BUILDKIT_HOST=docker-container://buildkit;
${command} >> ${logPath} 2>> ${logPath} || {
echo "❌ Railpack build failed" >> ${logPath};
exit 1;
}
echo "✅ Railpack build completed." >> ${logPath};
`;
return bashCommand;
};

View File

@@ -31,7 +31,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
mounts,
} = mariadb;
const defaultMariadbEnv = `MARIADB_DATABASE=${databaseName}\nMARIADB_USER=${databaseUser}\nMARIADB_PASSWORD=${databasePassword}\nMARIADB_ROOT_PASSWORD=${databaseRootPassword}${
const defaultMariadbEnv = `MARIADB_DATABASE="${databaseName}"\nMARIADB_USER="${databaseUser}"\nMARIADB_PASSWORD="${databasePassword}"\nMARIADB_ROOT_PASSWORD="${databaseRootPassword}"${
env ? `\n${env}` : ""
}`;
const resources = calculateResources({
@@ -98,7 +98,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (error) {
} catch (_error) {
await docker.createService(settings);
}
};

View File

@@ -77,7 +77,7 @@ fi
${command ?? "wait $MONGOD_PID"}`;
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${replicaSets ? "\nMONGO_INITDB_DATABASE=admin" : ""}${
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME="${databaseUser}"\nMONGO_INITDB_ROOT_PASSWORD="${databasePassword}"${replicaSets ? "\nMONGO_INITDB_DATABASE=admin" : ""}${
env ? `\n${env}` : ""
}`;
@@ -152,7 +152,7 @@ ${command ?? "wait $MONGOD_PID"}`;
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (error) {
} catch (_error) {
await docker.createService(settings);
}
};

View File

@@ -34,10 +34,10 @@ export const buildMysql = async (mysql: MysqlNested) => {
const defaultMysqlEnv =
databaseUser !== "root"
? `MYSQL_USER=${databaseUser}\nMYSQL_DATABASE=${databaseName}\nMYSQL_PASSWORD=${databasePassword}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
? `MYSQL_USER="${databaseUser}"\nMYSQL_DATABASE="${databaseName}"\nMYSQL_PASSWORD="${databasePassword}"\nMYSQL_ROOT_PASSWORD="${databaseRootPassword}"${
env ? `\n${env}` : ""
}`
: `MYSQL_DATABASE=${databaseName}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
: `MYSQL_DATABASE="${databaseName}"\nMYSQL_ROOT_PASSWORD="${databaseRootPassword}"${
env ? `\n${env}` : ""
}`;
const resources = calculateResources({
@@ -104,7 +104,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (error) {
} catch (_error) {
await docker.createService(settings);
}
};

View File

@@ -30,7 +30,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
mounts,
} = postgres;
const defaultPostgresEnv = `POSTGRES_DB=${databaseName}\nPOSTGRES_USER=${databaseUser}\nPOSTGRES_PASSWORD=${databasePassword}${
const defaultPostgresEnv = `POSTGRES_DB="${databaseName}"\nPOSTGRES_USER="${databaseUser}"\nPOSTGRES_PASSWORD="${databasePassword}"${
env ? `\n${env}` : ""
}`;
const resources = calculateResources({

Some files were not shown because too many files have changed in this diff Show More