mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-30 08:42:16 +00:00
Merge branch 'canary' into fix/gitlab-branches-pagination
This commit is contained in:
@@ -1,119 +0,0 @@
|
||||
import { webcrypto } from "node:crypto";
|
||||
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";
|
||||
|
||||
globalThis.crypto = webcrypto as Crypto;
|
||||
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;
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -5,35 +5,25 @@ export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
||||
export const docker = new Docker();
|
||||
|
||||
export const paths = (isServer = false) => {
|
||||
if (isServer) {
|
||||
const BASE_PATH = "/etc/dokploy";
|
||||
return {
|
||||
BASE_PATH,
|
||||
MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`,
|
||||
DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`,
|
||||
LOGS_PATH: `${BASE_PATH}/logs`,
|
||||
APPLICATIONS_PATH: `${BASE_PATH}/applications`,
|
||||
COMPOSE_PATH: `${BASE_PATH}/compose`,
|
||||
SSH_PATH: `${BASE_PATH}/ssh`,
|
||||
CERTIFICATES_PATH: `${BASE_PATH}/certificates`,
|
||||
MONITORING_PATH: `${BASE_PATH}/monitoring`,
|
||||
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
||||
};
|
||||
}
|
||||
const BASE_PATH =
|
||||
process.env.NODE_ENV === "production"
|
||||
isServer || process.env.NODE_ENV === "production"
|
||||
? "/etc/dokploy"
|
||||
: path.join(process.cwd(), ".docker");
|
||||
const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`;
|
||||
const DYNAMIC_TRAEFIK_PATH = `${MAIN_TRAEFIK_PATH}/dynamic`;
|
||||
|
||||
return {
|
||||
BASE_PATH,
|
||||
MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`,
|
||||
DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`,
|
||||
MAIN_TRAEFIK_PATH,
|
||||
DYNAMIC_TRAEFIK_PATH,
|
||||
LOGS_PATH: `${BASE_PATH}/logs`,
|
||||
APPLICATIONS_PATH: `${BASE_PATH}/applications`,
|
||||
COMPOSE_PATH: `${BASE_PATH}/compose`,
|
||||
SSH_PATH: `${BASE_PATH}/ssh`,
|
||||
CERTIFICATES_PATH: `${BASE_PATH}/certificates`,
|
||||
CERTIFICATES_PATH: `${DYNAMIC_TRAEFIK_PATH}/certificates`,
|
||||
MONITORING_PATH: `${BASE_PATH}/monitoring`,
|
||||
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
||||
SCHEDULES_PATH: `${BASE_PATH}/schedules`,
|
||||
VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const clearDb = async (): Promise<void> => {
|
||||
console.log(tables);
|
||||
await pg.end();
|
||||
} catch (error) {
|
||||
console.error("Error to clean database", error);
|
||||
console.error("Error cleaning database", error);
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
}));
|
||||
@@ -1,119 +0,0 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, 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),
|
||||
});
|
||||
|
||||
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(),
|
||||
});
|
||||
@@ -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(),
|
||||
});
|
||||
@@ -13,26 +13,27 @@ import { z } from "zod";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
import { ports } from "./port";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
import { projects } from "./project";
|
||||
import { redirects } from "./redirects";
|
||||
import { registry } from "./registry";
|
||||
import { security } from "./security";
|
||||
import { server } from "./server";
|
||||
import { applicationStatus, certificateType } from "./shared";
|
||||
import { applicationStatus, certificateType, triggerType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", [
|
||||
"docker",
|
||||
"git",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"drop",
|
||||
]);
|
||||
|
||||
@@ -42,9 +43,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 +117,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,26 +126,34 @@ export const applications = pgTable("application", {
|
||||
previewCertificateType: certificateType("certificateType")
|
||||
.notNull()
|
||||
.default("none"),
|
||||
previewCustomCertResolver: text("previewCustomCertResolver"),
|
||||
previewLimit: integer("previewLimit").default(3),
|
||||
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
|
||||
false,
|
||||
),
|
||||
// Security: Require collaborator permissions for preview deployments
|
||||
previewRequireCollaboratorPermissions: boolean(
|
||||
"previewRequireCollaboratorPermissions",
|
||||
).default(true),
|
||||
rollbackActive: boolean("rollbackActive").default(false),
|
||||
buildArgs: text("buildArgs"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
title: text("title"),
|
||||
enabled: boolean("enabled"),
|
||||
subtitle: text("subtitle"),
|
||||
command: text("command"),
|
||||
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
||||
sourceType: sourceType("sourceType").notNull().default("github"),
|
||||
cleanCache: boolean("cleanCache").default(false),
|
||||
// Github
|
||||
repository: text("repository"),
|
||||
owner: text("owner"),
|
||||
branch: text("branch"),
|
||||
buildPath: text("buildPath").default("/"),
|
||||
triggerType: triggerType("triggerType").default("push"),
|
||||
autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
|
||||
// Gitlab
|
||||
gitlabProjectId: integer("gitlabProjectId"),
|
||||
@@ -152,6 +162,11 @@ export const applications = pgTable("application", {
|
||||
gitlabBranch: text("gitlabBranch"),
|
||||
gitlabBuildPath: text("gitlabBuildPath").default("/"),
|
||||
gitlabPathNamespace: text("gitlabPathNamespace"),
|
||||
// Gitea
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
giteaBuildPath: text("giteaBuildPath").default("/"),
|
||||
// Bitbucket
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
@@ -172,6 +187,7 @@ export const applications = pgTable("application", {
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
|
||||
dockerfile: text("dockerfile"),
|
||||
dockerContextPath: text("dockerContextPath"),
|
||||
dockerBuildStage: text("dockerBuildStage"),
|
||||
@@ -194,6 +210,7 @@ export const applications = pgTable("application", {
|
||||
buildType: buildType("buildType").notNull().default("nixpacks"),
|
||||
herokuVersion: text("herokuVersion").default("24"),
|
||||
publishDirectory: text("publishDirectory"),
|
||||
isStaticSpa: boolean("isStaticSpa"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -209,6 +226,9 @@ export const applications = pgTable("application", {
|
||||
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
@@ -246,6 +266,10 @@ export const applicationsRelations = relations(
|
||||
fields: [applications.gitlabId],
|
||||
references: [gitlab.gitlabId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [applications.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
bitbucket: one(bitbucket, {
|
||||
fields: [applications.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
@@ -355,10 +379,10 @@ const createSchema = createInsertSchema(applications, {
|
||||
buildArgs: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
subtitle: z.string().optional(),
|
||||
@@ -376,7 +400,9 @@ const createSchema = createInsertSchema(applications, {
|
||||
customGitUrl: z.string().optional(),
|
||||
buildPath: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
sourceType: z.enum(["github", "docker", "git"]).optional(),
|
||||
sourceType: z
|
||||
.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"])
|
||||
.optional(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
buildType: z.enum([
|
||||
"dockerfile",
|
||||
@@ -384,9 +410,11 @@ const createSchema = createInsertSchema(applications, {
|
||||
"paketo_buildpacks",
|
||||
"nixpacks",
|
||||
"static",
|
||||
"railpack",
|
||||
]),
|
||||
herokuVersion: z.string().optional(),
|
||||
publishDirectory: z.string().optional(),
|
||||
isStaticSpa: z.boolean().optional(),
|
||||
owner: z.string(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
@@ -403,7 +431,10 @@ 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(),
|
||||
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
cleanCache: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateApplication = createSchema.pick({
|
||||
@@ -437,7 +468,7 @@ export const apiSaveBuildType = createSchema
|
||||
herokuVersion: true,
|
||||
})
|
||||
.required()
|
||||
.merge(createSchema.pick({ publishDirectory: true }));
|
||||
.merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));
|
||||
|
||||
export const apiSaveGithubProvider = createSchema
|
||||
.pick({
|
||||
@@ -447,8 +478,13 @@ export const apiSaveGithubProvider = createSchema
|
||||
owner: true,
|
||||
buildPath: true,
|
||||
githubId: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.extend({
|
||||
triggerType: z.enum(["push", "tag"]).default("push"),
|
||||
});
|
||||
|
||||
export const apiSaveGitlabProvider = createSchema
|
||||
.pick({
|
||||
@@ -460,6 +496,8 @@ export const apiSaveGitlabProvider = createSchema
|
||||
gitlabId: true,
|
||||
gitlabProjectId: true,
|
||||
gitlabPathNamespace: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -471,6 +509,21 @@ export const apiSaveBitbucketProvider = createSchema
|
||||
bitbucketRepository: true,
|
||||
bitbucketId: true,
|
||||
applicationId: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveGiteaProvider = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
giteaBranch: true,
|
||||
giteaBuildPath: true,
|
||||
giteaOwner: true,
|
||||
giteaRepository: true,
|
||||
giteaId: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -490,6 +543,8 @@ export const apiSaveGitProvider = createSchema
|
||||
applicationId: true,
|
||||
customGitBuildPath: true,
|
||||
customGitUrl: true,
|
||||
watchPaths: true,
|
||||
enableSubmodules: true,
|
||||
})
|
||||
.required()
|
||||
.merge(
|
||||
|
||||
@@ -1,129 +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(),
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -2,6 +2,8 @@ import { relations } from "drizzle-orm";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
text,
|
||||
@@ -9,34 +11,51 @@ import {
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { generateAppName } from ".";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
|
||||
import { users_temp } from "./user";
|
||||
export const databaseType = pgEnum("databaseType", [
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
]);
|
||||
|
||||
export const backupType = pgEnum("backupType", ["database", "compose"]);
|
||||
|
||||
export const backups = pgTable("backup", {
|
||||
backupId: text("backupId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("backup"))
|
||||
.unique(),
|
||||
schedule: text("schedule").notNull(),
|
||||
enabled: boolean("enabled"),
|
||||
database: text("database").notNull(),
|
||||
prefix: text("prefix").notNull(),
|
||||
|
||||
serviceName: text("serviceName"),
|
||||
destinationId: text("destinationId")
|
||||
.notNull()
|
||||
.references(() => destinations.destinationId, { onDelete: "cascade" }),
|
||||
|
||||
keepLatestCount: integer("keepLatestCount"),
|
||||
backupType: backupType("backupType").notNull().default("database"),
|
||||
databaseType: databaseType("databaseType").notNull(),
|
||||
composeId: text("composeId").references(
|
||||
(): AnyPgColumn => compose.composeId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
},
|
||||
),
|
||||
postgresId: text("postgresId").references(
|
||||
(): AnyPgColumn => postgres.postgresId,
|
||||
{
|
||||
@@ -55,9 +74,30 @@ export const backups = pgTable("backup", {
|
||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => users_temp.id),
|
||||
// Only for compose backups
|
||||
metadata: jsonb("metadata").$type<
|
||||
| {
|
||||
postgres?: {
|
||||
databaseUser: string;
|
||||
};
|
||||
mariadb?: {
|
||||
databaseUser: string;
|
||||
databasePassword: string;
|
||||
};
|
||||
mongo?: {
|
||||
databaseUser: string;
|
||||
databasePassword: string;
|
||||
};
|
||||
mysql?: {
|
||||
databaseRootPassword: string;
|
||||
};
|
||||
}
|
||||
| undefined
|
||||
>(),
|
||||
});
|
||||
|
||||
export const backupsRelations = relations(backups, ({ one }) => ({
|
||||
export const backupsRelations = relations(backups, ({ one, many }) => ({
|
||||
destination: one(destinations, {
|
||||
fields: [backups.destinationId],
|
||||
references: [destinations.destinationId],
|
||||
@@ -78,6 +118,15 @@ export const backupsRelations = relations(backups, ({ one }) => ({
|
||||
fields: [backups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
fields: [backups.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [backups.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(backups, {
|
||||
@@ -87,11 +136,14 @@ const createSchema = createInsertSchema(backups, {
|
||||
prefix: z.string().min(1),
|
||||
database: z.string().min(1),
|
||||
schedule: z.string(),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo"]),
|
||||
keepLatestCount: z.number().optional(),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
|
||||
postgresId: z.string().optional(),
|
||||
mariadbId: z.string().optional(),
|
||||
mysqlId: z.string().optional(),
|
||||
mongoId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
metadata: z.any().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateBackup = createSchema.pick({
|
||||
@@ -99,12 +151,18 @@ export const apiCreateBackup = createSchema.pick({
|
||||
enabled: true,
|
||||
prefix: true,
|
||||
destinationId: true,
|
||||
keepLatestCount: true,
|
||||
database: true,
|
||||
mariadbId: true,
|
||||
mysqlId: true,
|
||||
postgresId: true,
|
||||
mongoId: true,
|
||||
databaseType: true,
|
||||
userId: true,
|
||||
backupType: true,
|
||||
composeId: true,
|
||||
serviceName: true,
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
export const apiFindOneBackup = createSchema
|
||||
@@ -127,5 +185,45 @@ export const apiUpdateBackup = createSchema
|
||||
backupId: true,
|
||||
destinationId: true,
|
||||
database: true,
|
||||
keepLatestCount: true,
|
||||
serviceName: true,
|
||||
metadata: true,
|
||||
databaseType: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRestoreBackup = z.object({
|
||||
databaseId: z.string(),
|
||||
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
|
||||
backupType: z.enum(["database", "compose"]),
|
||||
databaseName: z.string().min(1),
|
||||
backupFile: z.string().min(1),
|
||||
destinationId: z.string().min(1),
|
||||
metadata: z
|
||||
.object({
|
||||
serviceName: z.string().optional(),
|
||||
postgres: z
|
||||
.object({
|
||||
databaseUser: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
mariadb: z
|
||||
.object({
|
||||
databaseUser: z.string(),
|
||||
databasePassword: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
mongo: z
|
||||
.object({
|
||||
databaseUser: z.string(),
|
||||
databasePassword: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
mysql: z
|
||||
.object({
|
||||
databaseRootPassword: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -3,23 +3,27 @@ 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 { backups } from "./backups";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { applicationStatus, triggerType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
import { schedules } from "./schedule";
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
"git",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"raw",
|
||||
]);
|
||||
|
||||
@@ -55,6 +59,10 @@ export const compose = pgTable("compose", {
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
bitbucketBranch: text("bitbucketBranch"),
|
||||
// Gitea
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
// Git
|
||||
customGitUrl: text("customGitUrl"),
|
||||
customGitBranch: text("customGitBranch"),
|
||||
@@ -66,9 +74,16 @@ export const compose = pgTable("compose", {
|
||||
),
|
||||
command: text("command").notNull().default(""),
|
||||
//
|
||||
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
|
||||
composePath: text("composePath").notNull().default("./docker-compose.yml"),
|
||||
suffix: text("suffix").notNull().default(""),
|
||||
randomize: boolean("randomize").notNull().default(false),
|
||||
isolatedDeployment: boolean("isolatedDeployment").notNull().default(false),
|
||||
// Keep this for backward compatibility since we will not add the prefix anymore to volumes
|
||||
isolatedDeploymentsVolume: boolean("isolatedDeploymentsVolume")
|
||||
.notNull()
|
||||
.default(false),
|
||||
triggerType: triggerType("triggerType").default("push"),
|
||||
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
@@ -76,7 +91,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",
|
||||
}),
|
||||
@@ -86,6 +101,9 @@ export const compose = pgTable("compose", {
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -115,22 +133,29 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
fields: [compose.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [compose.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
server: one(server, {
|
||||
fields: [compose.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(compose, {
|
||||
name: z.string().min(1),
|
||||
description: z.string(),
|
||||
env: z.string().optional(),
|
||||
composeFile: z.string().min(1),
|
||||
composeFile: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
customGitSSHKeyId: z.string().optional(),
|
||||
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({
|
||||
@@ -140,6 +165,7 @@ export const apiCreateCompose = createSchema.pick({
|
||||
composeType: true,
|
||||
appName: true,
|
||||
serverId: true,
|
||||
composeFile: true,
|
||||
});
|
||||
|
||||
export const apiCreateComposeByTemplate = createSchema
|
||||
@@ -155,6 +181,11 @@ export const apiFindCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiDeleteCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
deleteVolumes: z.boolean(),
|
||||
});
|
||||
|
||||
export const apiFetchServices = z.object({
|
||||
composeId: z.string().min(1),
|
||||
type: z.enum(["fetch", "cache"]).optional().default("cache"),
|
||||
|
||||
@@ -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 });
|
||||
@@ -1,4 +1,4 @@
|
||||
import { is, relations } from "drizzle-orm";
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
boolean,
|
||||
@@ -10,10 +10,13 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { backups } from "./backups";
|
||||
import { compose } from "./compose";
|
||||
import { server } from "./server";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
|
||||
import { rollbacks } from "./rollbacks";
|
||||
import { schedules } from "./schedule";
|
||||
import { server } from "./server";
|
||||
import { volumeBackups } from "./volume-backups";
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
"done",
|
||||
@@ -29,6 +32,7 @@ export const deployments = pgTable("deployment", {
|
||||
description: text("description"),
|
||||
status: deploymentStatus("status").default("running"),
|
||||
logPath: text("logPath").notNull(),
|
||||
pid: text("pid"),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
@@ -47,6 +51,24 @@ export const deployments = pgTable("deployment", {
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
startedAt: text("startedAt"),
|
||||
finishedAt: text("finishedAt"),
|
||||
errorMessage: text("errorMessage"),
|
||||
scheduleId: text("scheduleId").references(
|
||||
(): AnyPgColumn => schedules.scheduleId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
backupId: text("backupId").references((): AnyPgColumn => backups.backupId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
rollbackId: text("rollbackId").references(
|
||||
(): AnyPgColumn => rollbacks.rollbackId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
volumeBackupId: text("volumeBackupId").references(
|
||||
(): AnyPgColumn => volumeBackups.volumeBackupId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
});
|
||||
|
||||
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
@@ -66,6 +88,22 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
fields: [deployments.previewDeploymentId],
|
||||
references: [previewDeployments.previewDeploymentId],
|
||||
}),
|
||||
schedule: one(schedules, {
|
||||
fields: [deployments.scheduleId],
|
||||
references: [schedules.scheduleId],
|
||||
}),
|
||||
backup: one(backups, {
|
||||
fields: [deployments.backupId],
|
||||
references: [backups.backupId],
|
||||
}),
|
||||
rollback: one(rollbacks, {
|
||||
fields: [deployments.deploymentId],
|
||||
references: [rollbacks.deploymentId],
|
||||
}),
|
||||
volumeBackup: one(volumeBackups, {
|
||||
fields: [deployments.volumeBackupId],
|
||||
references: [volumeBackups.volumeBackupId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const schema = createInsertSchema(deployments, {
|
||||
@@ -115,6 +153,18 @@ export const apiCreateDeploymentCompose = schema
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentBackup = schema
|
||||
.pick({
|
||||
title: true,
|
||||
status: true,
|
||||
logPath: true,
|
||||
backupId: true,
|
||||
description: true,
|
||||
})
|
||||
.extend({
|
||||
backupId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentServer = schema
|
||||
.pick({
|
||||
title: true,
|
||||
@@ -127,6 +177,28 @@ export const apiCreateDeploymentServer = schema
|
||||
serverId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentSchedule = schema
|
||||
.pick({
|
||||
title: true,
|
||||
status: true,
|
||||
logPath: true,
|
||||
description: true,
|
||||
})
|
||||
.extend({
|
||||
scheduleId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiCreateDeploymentVolumeBackup = schema
|
||||
.pick({
|
||||
title: true,
|
||||
status: true,
|
||||
logPath: true,
|
||||
description: true,
|
||||
})
|
||||
.extend({
|
||||
volumeBackupId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindAllByApplication = schema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
@@ -153,3 +225,18 @@ export const apiFindAllByServer = schema
|
||||
serverId: z.string().min(1),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiFindAllByType = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
type: z.enum([
|
||||
"application",
|
||||
"compose",
|
||||
"server",
|
||||
"schedule",
|
||||
"previewDeployment",
|
||||
"backup",
|
||||
"volumeBackup",
|
||||
]),
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -82,6 +82,7 @@ export const apiUpdateDestination = createSchema
|
||||
endpoint: true,
|
||||
secretAccessKey: true,
|
||||
destinationId: true,
|
||||
provider: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
|
||||
@@ -14,8 +14,8 @@ import { z } from "zod";
|
||||
import { domain } from "../validations/domain";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { certificateType } from "./shared";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
import { certificateType } from "./shared";
|
||||
|
||||
export const domainType = pgEnum("domainType", [
|
||||
"compose",
|
||||
@@ -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" },
|
||||
@@ -50,6 +51,8 @@ export const domains = pgTable("domain", {
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||
internalPath: text("internalPath").default("/"),
|
||||
stripPath: boolean("stripPath").notNull().default(false),
|
||||
});
|
||||
|
||||
export const domainsRelations = relations(domains, ({ one }) => ({
|
||||
@@ -76,10 +79,13 @@ export const apiCreateDomain = createSchema.pick({
|
||||
https: true,
|
||||
applicationId: true,
|
||||
certificateType: true,
|
||||
customCertResolver: true,
|
||||
composeId: true,
|
||||
serviceName: true,
|
||||
domainType: true,
|
||||
previewDeploymentId: true,
|
||||
internalPath: true,
|
||||
stripPath: true,
|
||||
});
|
||||
|
||||
export const apiFindDomain = createSchema
|
||||
@@ -107,7 +113,10 @@ export const apiUpdateDomain = createSchema
|
||||
port: true,
|
||||
https: true,
|
||||
certificateType: true,
|
||||
customCertResolver: true,
|
||||
serviceName: true,
|
||||
domainType: true,
|
||||
internalPath: true,
|
||||
stripPath: true,
|
||||
})
|
||||
.merge(createSchema.pick({ domainId: true }).required());
|
||||
|
||||
@@ -3,15 +3,18 @@ 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 { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { users_temp } from "./user";
|
||||
|
||||
export const gitProviderType = pgEnum("gitProviderType", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
]);
|
||||
|
||||
export const gitProvider = pgTable("git_provider", {
|
||||
@@ -24,12 +27,15 @@ 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" }),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users_temp.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 +48,17 @@ export const gitProviderRelations = relations(gitProvider, ({ one, many }) => ({
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [bitbucket.gitProviderId],
|
||||
}),
|
||||
admin: one(admins, {
|
||||
fields: [gitProvider.adminId],
|
||||
references: [admins.adminId],
|
||||
gitea: one(gitea, {
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [gitea.gitProviderId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [gitProvider.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
fields: [gitProvider.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { gitProvider } from "./git-provider";
|
||||
|
||||
export const gitea = pgTable("gitea", {
|
||||
giteaId: text("giteaId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(),
|
||||
redirectUri: text("redirect_uri"),
|
||||
clientId: text("client_id"),
|
||||
clientSecret: text("client_secret"),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
expiresAt: integer("expires_at"),
|
||||
scopes: text("scopes").default("repo,repo:status,read:user,read:org"),
|
||||
lastAuthenticatedAt: integer("last_authenticated_at"),
|
||||
});
|
||||
|
||||
export const giteaProviderRelations = relations(gitea, ({ one }) => ({
|
||||
gitProvider: one(gitProvider, {
|
||||
fields: [gitea.gitProviderId],
|
||||
references: [gitProvider.gitProviderId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(gitea);
|
||||
|
||||
export const apiCreateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
gitProviderId: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneGitea = createSchema
|
||||
.extend({
|
||||
giteaId: z.string().min(1),
|
||||
})
|
||||
.pick({ giteaId: true });
|
||||
|
||||
export const apiGiteaTestConnection = createSchema
|
||||
.extend({
|
||||
organizationName: z.string().optional(),
|
||||
})
|
||||
.pick({ giteaId: true, organizationName: true });
|
||||
|
||||
export type ApiGiteaTestConnection = z.infer<typeof apiGiteaTestConnection>;
|
||||
|
||||
export const apiFindGiteaBranches = z.object({
|
||||
owner: z.string().min(1),
|
||||
repositoryName: z.string().min(1),
|
||||
giteaId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaId: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
@@ -10,6 +10,7 @@ export const gitlab = pgTable("gitlab", {
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
gitlabUrl: text("gitlabUrl").default("https://gitlab.com").notNull(),
|
||||
applicationId: text("application_id"),
|
||||
redirectUri: text("redirect_uri"),
|
||||
secret: text("secret"),
|
||||
@@ -39,6 +40,7 @@ export const apiCreateGitlab = createSchema.extend({
|
||||
redirectUri: z.string().optional(),
|
||||
authId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
gitlabUrl: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneGitlab = createSchema
|
||||
@@ -67,4 +69,5 @@ export const apiUpdateGitlab = createSchema.extend({
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
gitlabId: z.string().min(1),
|
||||
gitlabUrl: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
@@ -27,6 +25,12 @@ export * from "./git-provider";
|
||||
export * from "./bitbucket";
|
||||
export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./gitea";
|
||||
export * from "./server";
|
||||
export * from "./utils";
|
||||
export * from "./preview-deployments";
|
||||
export * from "./preview-deployments";
|
||||
export * from "./ai";
|
||||
export * from "./account";
|
||||
export * from "./schedule";
|
||||
export * from "./rollbacks";
|
||||
export * from "./volume-backups";
|
||||
|
||||
@@ -29,10 +29,10 @@ export const mariadb = pgTable("mariadb", {
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
// RESOURCES
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
//
|
||||
externalPort: integer("externalPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
@@ -74,10 +74,10 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
dockerImage: z.string().default("mariadb:6"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
@@ -146,3 +146,9 @@ export const apiUpdateMariaDB = createSchema
|
||||
mariadbId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildMariadb = createSchema
|
||||
.pick({
|
||||
mariadbId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -26,10 +26,10 @@ export const mongo = pgTable("mongo", {
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
externalPort: integer("externalPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
@@ -43,6 +43,7 @@ export const mongo = pgTable("mongo", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
replicaSets: boolean("replicaSets").default(false),
|
||||
});
|
||||
|
||||
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||
@@ -68,15 +69,16 @@ const createSchema = createInsertSchema(mongo, {
|
||||
dockerImage: z.string().default("mongo:15"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
replicaSets: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const apiCreateMongo = createSchema
|
||||
@@ -89,6 +91,7 @@ export const apiCreateMongo = createSchema
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
serverId: true,
|
||||
replicaSets: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -138,3 +141,9 @@ export const apiResetMongo = createSchema
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRebuildMongo = createSchema
|
||||
.pick({
|
||||
mongoId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -28,10 +28,10 @@ export const mysql = pgTable("mysql", {
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
externalPort: integer("externalPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
@@ -72,10 +72,10 @@ const createSchema = createInsertSchema(mysql, {
|
||||
dockerImage: z.string().default("mysql:8"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
@@ -144,3 +144,9 @@ export const apiUpdateMySql = createSchema
|
||||
mysqlId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildMysql = createSchema
|
||||
.pick({
|
||||
mysqlId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -3,13 +3,14 @@ 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",
|
||||
"telegram",
|
||||
"discord",
|
||||
"email",
|
||||
"gotify",
|
||||
]);
|
||||
|
||||
export const notifications = pgTable("notification", {
|
||||
@@ -23,6 +24,7 @@ export const notifications = pgTable("notification", {
|
||||
databaseBackup: boolean("databaseBackup").notNull().default(false),
|
||||
dokployRestart: boolean("dokployRestart").notNull().default(false),
|
||||
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
|
||||
serverThreshold: boolean("serverThreshold").notNull().default(false),
|
||||
notificationType: notificationType("notificationType").notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -39,9 +41,12 @@ export const notifications = pgTable("notification", {
|
||||
emailId: text("emailId").references(() => email.emailId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
adminId: text("adminId").references(() => admins.adminId, {
|
||||
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const slack = pgTable("slack", {
|
||||
@@ -60,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", {
|
||||
@@ -68,6 +74,7 @@ export const discord = pgTable("discord", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
decoration: boolean("decoration"),
|
||||
});
|
||||
|
||||
export const email = pgTable("email", {
|
||||
@@ -83,6 +90,17 @@ export const email = pgTable("email", {
|
||||
toAddresses: text("toAddress").array().notNull(),
|
||||
});
|
||||
|
||||
export const gotify = pgTable("gotify", {
|
||||
gotifyId: text("gotifyId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
serverUrl: text("serverUrl").notNull(),
|
||||
appToken: text("appToken").notNull(),
|
||||
priority: integer("priority").notNull().default(5),
|
||||
decoration: boolean("decoration"),
|
||||
});
|
||||
|
||||
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
slack: one(slack, {
|
||||
fields: [notifications.slackId],
|
||||
@@ -100,9 +118,13 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.emailId],
|
||||
references: [email.emailId],
|
||||
}),
|
||||
admin: one(admins, {
|
||||
fields: [notifications.adminId],
|
||||
references: [admins.adminId],
|
||||
gotify: one(gotify, {
|
||||
fields: [notifications.gotifyId],
|
||||
references: [gotify.gotifyId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [notifications.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -116,6 +138,7 @@ export const apiCreateSlack = notificationsSchema
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
@@ -126,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({
|
||||
@@ -142,22 +165,25 @@ export const apiCreateTelegram = notificationsSchema
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.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
|
||||
@@ -168,21 +194,27 @@ export const apiCreateDiscord = notificationsSchema
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
decoration: z.boolean(),
|
||||
})
|
||||
.required();
|
||||
|
||||
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.pick({
|
||||
webhookUrl: true,
|
||||
});
|
||||
export const apiTestDiscordConnection = apiCreateDiscord
|
||||
.pick({
|
||||
webhookUrl: true,
|
||||
})
|
||||
.extend({
|
||||
decoration: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateEmail = notificationsSchema
|
||||
.pick({
|
||||
@@ -192,6 +224,7 @@ export const apiCreateEmail = notificationsSchema
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
smtpServer: z.string().min(1),
|
||||
@@ -206,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({
|
||||
@@ -218,6 +251,39 @@ export const apiTestEmailConnection = apiCreateEmail.pick({
|
||||
fromAddress: true,
|
||||
});
|
||||
|
||||
export const apiCreateGotify = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
})
|
||||
.extend({
|
||||
serverUrl: z.string().min(1),
|
||||
appToken: z.string().min(1),
|
||||
priority: z.number().min(1),
|
||||
decoration: z.boolean(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateGotify = apiCreateGotify.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
gotifyId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestGotifyConnection = apiCreateGotify
|
||||
.pick({
|
||||
serverUrl: true,
|
||||
appToken: true,
|
||||
priority: true,
|
||||
})
|
||||
.extend({
|
||||
decoration: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneNotification = notificationsSchema
|
||||
.pick({
|
||||
notificationId: true,
|
||||
@@ -236,5 +302,8 @@ export const apiSendTest = notificationsSchema
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
toAddresses: z.array(z.string()),
|
||||
serverUrl: z.string(),
|
||||
appToken: z.string(),
|
||||
priority: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
|
||||
export const protocolType = pgEnum("protocolType", ["tcp", "udp"]);
|
||||
export const publishModeType = pgEnum("publishModeType", ["ingress", "host"]);
|
||||
|
||||
export const ports = pgTable("port", {
|
||||
portId: text("portId")
|
||||
@@ -13,6 +14,7 @@ export const ports = pgTable("port", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
publishedPort: integer("publishedPort").notNull(),
|
||||
publishMode: publishModeType("publishMode").notNull().default("host"),
|
||||
targetPort: integer("targetPort").notNull(),
|
||||
protocol: protocolType("protocol").notNull(),
|
||||
|
||||
@@ -32,6 +34,7 @@ const createSchema = createInsertSchema(ports, {
|
||||
portId: z.string().min(1),
|
||||
applicationId: z.string().min(1),
|
||||
publishedPort: z.number(),
|
||||
publishMode: z.enum(["ingress", "host"]).default("ingress"),
|
||||
targetPort: z.number(),
|
||||
protocol: z.enum(["tcp", "udp"]).default("tcp"),
|
||||
});
|
||||
@@ -39,6 +42,7 @@ const createSchema = createInsertSchema(ports, {
|
||||
export const apiCreatePort = createSchema
|
||||
.pick({
|
||||
publishedPort: true,
|
||||
publishMode: true,
|
||||
targetPort: true,
|
||||
protocol: true,
|
||||
applicationId: true,
|
||||
@@ -55,6 +59,7 @@ export const apiUpdatePort = createSchema
|
||||
.pick({
|
||||
portId: true,
|
||||
publishedPort: true,
|
||||
publishMode: true,
|
||||
targetPort: true,
|
||||
protocol: true,
|
||||
})
|
||||
|
||||
@@ -27,11 +27,11 @@ export const postgres = pgTable("postgres", {
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
externalPort: integer("externalPort"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
@@ -68,10 +68,10 @@ const createSchema = createInsertSchema(postgres, {
|
||||
dockerImage: z.string().default("postgres:15"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
@@ -140,3 +140,9 @@ export const apiUpdatePostgres = createSchema
|
||||
postgresId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildPostgres = createSchema
|
||||
.pick({
|
||||
postgresId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { applications } from "./application";
|
||||
import { domains } from "./domain";
|
||||
import { deployments } from "./deployment";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { generateAppName } from "./utils";
|
||||
import { applications } from "./application";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const previewDeployments = pgTable("preview_deployments", {
|
||||
previewDeploymentId: text("previewDeploymentId")
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -52,6 +52,7 @@ const createSchema = createInsertSchema(projects, {
|
||||
export const apiCreateProject = createSchema.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
env: true,
|
||||
});
|
||||
|
||||
export const apiFindOneProject = createSchema
|
||||
|
||||
@@ -24,10 +24,10 @@ export const redis = pgTable("redis", {
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
externalPort: integer("externalPort"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -64,10 +64,10 @@ const createSchema = createInsertSchema(redis, {
|
||||
dockerImage: z.string().default("redis:8"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
@@ -133,3 +133,9 @@ export const apiUpdateRedis = createSchema
|
||||
redisId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildRedis = createSchema
|
||||
.pick({
|
||||
redisId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -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(),
|
||||
@@ -67,7 +63,7 @@ export const apiCreateRegistry = createSchema
|
||||
});
|
||||
|
||||
export const apiTestRegistry = createSchema.pick({}).extend({
|
||||
registryName: z.string().min(1),
|
||||
registryName: z.string().optional(),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
registryUrl: z.string(),
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { Application } from "@dokploy/server/services/application";
|
||||
import type { Mount } from "@dokploy/server/services/mount";
|
||||
import type { Port } from "@dokploy/server/services/port";
|
||||
import type { Project } from "@dokploy/server/services/project";
|
||||
import type { Registry } from "@dokploy/server/services/registry";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { deployments } from "./deployment";
|
||||
|
||||
export const rollbacks = pgTable("rollback", {
|
||||
rollbackId: text("rollbackId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
deploymentId: text("deploymentId")
|
||||
.notNull()
|
||||
.references(() => deployments.deploymentId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
version: serial(),
|
||||
image: text("image"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
fullContext: jsonb("fullContext").$type<
|
||||
Application & {
|
||||
project: Project;
|
||||
mounts: Mount[];
|
||||
ports: Port[];
|
||||
registry?: Registry | null;
|
||||
}
|
||||
>(),
|
||||
});
|
||||
|
||||
export type Rollback = typeof rollbacks.$inferSelect;
|
||||
|
||||
export const rollbacksRelations = relations(rollbacks, ({ one }) => ({
|
||||
deployment: one(deployments, {
|
||||
fields: [rollbacks.deploymentId],
|
||||
references: [deployments.deploymentId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const createRollbackSchema = createInsertSchema(rollbacks).extend({
|
||||
appName: z.string().min(1),
|
||||
});
|
||||
|
||||
export const updateRollbackSchema = createRollbackSchema.extend({
|
||||
rollbackId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneRollback = z.object({
|
||||
rollbackId: z.string().min(1),
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
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 { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { server } from "./server";
|
||||
import { users_temp } from "./user";
|
||||
import { generateAppName } from "./utils";
|
||||
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
|
||||
|
||||
export const scheduleType = pgEnum("scheduleType", [
|
||||
"application",
|
||||
"compose",
|
||||
"server",
|
||||
"dokploy-server",
|
||||
]);
|
||||
|
||||
export const schedules = pgTable("schedule", {
|
||||
scheduleId: text("scheduleId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
cronExpression: text("cronExpression").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("schedule")),
|
||||
serviceName: text("serviceName"),
|
||||
shellType: shellTypes("shellType").notNull().default("bash"),
|
||||
scheduleType: scheduleType("scheduleType").notNull().default("application"),
|
||||
command: text("command").notNull(),
|
||||
script: text("script"),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
},
|
||||
),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => users_temp.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
});
|
||||
|
||||
export type Schedule = typeof schedules.$inferSelect;
|
||||
|
||||
export const schedulesRelations = relations(schedules, ({ one, many }) => ({
|
||||
application: one(applications, {
|
||||
fields: [schedules.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [schedules.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
server: one(server, {
|
||||
fields: [schedules.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
fields: [schedules.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
}));
|
||||
|
||||
export const createScheduleSchema = createInsertSchema(schedules);
|
||||
|
||||
export const updateScheduleSchema = createScheduleSchema.extend({
|
||||
scheduleId: z.string().min(1),
|
||||
});
|
||||
@@ -0,0 +1,897 @@
|
||||
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
|
||||
gitea
|
||||
}
|
||||
|
||||
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
|
||||
gitea
|
||||
drop
|
||||
}
|
||||
|
||||
enum sourceTypeCompose {
|
||||
git
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
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']
|
||||
cleanCache boolean [default: false]
|
||||
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
|
||||
giteaId 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
|
||||
giteaRepository text
|
||||
giteaOwner text
|
||||
giteaBranch 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
|
||||
giteaId 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 gitea {
|
||||
giteaId text [pk, not null]
|
||||
giteaUrl text [not null, default: 'https://gitea.com']
|
||||
redirect_uri text
|
||||
client_id text [not null]
|
||||
client_secret text [not null]
|
||||
access_token text
|
||||
refresh_token text
|
||||
expires_at integer
|
||||
gitProviderId text [not null]
|
||||
scopes text [default: 'repo,repo:status,read:user,read:org']
|
||||
last_authenticated_at integer
|
||||
}
|
||||
|
||||
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: gitea.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
|
||||
@@ -1,10 +1,16 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { admins } from "./admin";
|
||||
import { organization } from "./account";
|
||||
import { applications } from "./application";
|
||||
import { certificates } from "./certificate";
|
||||
import { compose } from "./compose";
|
||||
@@ -14,9 +20,9 @@ import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { redis } from "./redis";
|
||||
import { schedules } from "./schedule";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
|
||||
|
||||
export const server = pgTable("server", {
|
||||
@@ -33,24 +39,64 @@ 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, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
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: "Remote",
|
||||
refreshRate: 60,
|
||||
port: 4500,
|
||||
token: "",
|
||||
urlCallback: "",
|
||||
cronJob: "",
|
||||
retentionDays: 2,
|
||||
thresholds: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
},
|
||||
},
|
||||
containers: {
|
||||
refreshRate: 60,
|
||||
services: {
|
||||
include: [],
|
||||
exclude: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
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],
|
||||
@@ -64,6 +110,11 @@ 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],
|
||||
}),
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(server, {
|
||||
@@ -105,4 +156,38 @@ export const apiUpdateServer = createSchema
|
||||
username: true,
|
||||
sshKeyId: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.extend({
|
||||
command: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiUpdateServerMonitoring = createSchema
|
||||
.pick({
|
||||
serverId: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
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(),
|
||||
});
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
@@ -10,4 +10,7 @@ export const applicationStatus = pgEnum("applicationStatus", [
|
||||
export const certificateType = pgEnum("certificateType", [
|
||||
"letsencrypt",
|
||||
"none",
|
||||
"custom",
|
||||
]);
|
||||
|
||||
export const triggerType = pgEnum("triggerType", ["push", "tag"]);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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 }));
|
||||
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
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, apikey, organization } from "./account";
|
||||
import { backups } from "./backups";
|
||||
import { projects } from "./project";
|
||||
import { schedules } from "./schedule";
|
||||
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 +23,122 @@ 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"),
|
||||
https: boolean("https").notNull().default(false),
|
||||
host: text("host"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
role: text("role").notNull().default("user"),
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||
allowImpersonation: boolean("allowImpersonation").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),
|
||||
accesedProjects: text("accesedProjects")
|
||||
.array()
|
||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accesedServices: 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),
|
||||
backups: many(backups),
|
||||
schedules: many(schedules),
|
||||
}));
|
||||
|
||||
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(),
|
||||
accesedProjects: z.array(z.string()).optional(),
|
||||
accesedServices: 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(),
|
||||
}).omit({
|
||||
role: true,
|
||||
});
|
||||
|
||||
export const apiCreateUserInvitation = createSchema.pick({}).extend({
|
||||
@@ -89,41 +147,206 @@ 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,
|
||||
accesedProjects: true,
|
||||
accesedServices: 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,
|
||||
https: true,
|
||||
})
|
||||
.required()
|
||||
.partial({
|
||||
letsEncryptEmail: true,
|
||||
https: 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)
|
||||
.refine(
|
||||
(path) => {
|
||||
// Prevent directory traversal attacks
|
||||
if (path.includes("../") || path.includes("..\\")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { MAIN_TRAEFIK_PATH } = paths();
|
||||
if (path.startsWith("/") && !path.startsWith(MAIN_TRAEFIK_PATH)) {
|
||||
return false;
|
||||
}
|
||||
// Prevent null bytes and other dangerous characters
|
||||
if (path.includes("\0") || path.includes("\x00")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid path: path traversal or unauthorized directory access detected",
|
||||
},
|
||||
),
|
||||
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(),
|
||||
name: 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(),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
@@ -13,3 +14,17 @@ export const generateAppName = (type: string) => {
|
||||
const nanoidPart = customNanoid();
|
||||
return `${type}-${randomFakerElement}-${nanoidPart}`;
|
||||
};
|
||||
|
||||
export const cleanAppName = (appName?: string) => {
|
||||
if (!appName) {
|
||||
return appName?.toLowerCase();
|
||||
}
|
||||
return appName.trim().replace(/ /g, "-").toLowerCase();
|
||||
};
|
||||
|
||||
export const buildAppName = (type: string, baseAppName?: string) => {
|
||||
if (baseAppName) {
|
||||
return `${cleanAppName(baseAppName)}-${generatePassword(6)}`;
|
||||
}
|
||||
return generateAppName(type);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { serviceType } from "./mount";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { redis } from "./redis";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const volumeBackups = pgTable("volume_backup", {
|
||||
volumeBackupId: text("volumeBackupId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
volumeName: text("volumeName").notNull(),
|
||||
prefix: text("prefix").notNull(),
|
||||
serviceType: serviceType("serviceType").notNull().default("application"),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("volumeBackup")),
|
||||
serviceName: text("serviceName"),
|
||||
turnOff: boolean("turnOff").notNull().default(false),
|
||||
cronExpression: text("cronExpression").notNull(),
|
||||
keepLatestCount: integer("keepLatestCount"),
|
||||
enabled: boolean("enabled"),
|
||||
applicationId: text("applicationId").references(
|
||||
() => applications.applicationId,
|
||||
{
|
||||
onDelete: "cascade",
|
||||
},
|
||||
),
|
||||
postgresId: text("postgresId").references(() => postgres.postgresId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mariadbId: text("mariadbId").references(() => mariadb.mariadbId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mongoId: text("mongoId").references(() => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mysqlId: text("mysqlId").references(() => mysql.mysqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
destinationId: text("destinationId")
|
||||
.notNull()
|
||||
.references(() => destinations.destinationId, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export type VolumeBackup = typeof volumeBackups.$inferSelect;
|
||||
|
||||
export const volumeBackupsRelations = relations(
|
||||
volumeBackups,
|
||||
({ one, many }) => ({
|
||||
application: one(applications, {
|
||||
fields: [volumeBackups.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [volumeBackups.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
}),
|
||||
mariadb: one(mariadb, {
|
||||
fields: [volumeBackups.mariadbId],
|
||||
references: [mariadb.mariadbId],
|
||||
}),
|
||||
mongo: one(mongo, {
|
||||
fields: [volumeBackups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
mysql: one(mysql, {
|
||||
fields: [volumeBackups.mysqlId],
|
||||
references: [mysql.mysqlId],
|
||||
}),
|
||||
redis: one(redis, {
|
||||
fields: [volumeBackups.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [volumeBackups.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
destination: one(destinations, {
|
||||
fields: [volumeBackups.destinationId],
|
||||
references: [destinations.destinationId],
|
||||
}),
|
||||
deployments: many(deployments),
|
||||
}),
|
||||
);
|
||||
|
||||
export const createVolumeBackupSchema = createInsertSchema(volumeBackups).omit({
|
||||
volumeBackupId: true,
|
||||
});
|
||||
|
||||
export const updateVolumeBackupSchema = createVolumeBackupSchema.extend({
|
||||
volumeBackupId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneVolumeBackup = z.object({
|
||||
volumeBackupId: z.string().min(1),
|
||||
});
|
||||
@@ -4,13 +4,16 @@ export const domain = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Add a hostname" }),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
port: z
|
||||
.number()
|
||||
.min(1, { message: "Port must be at least 1" })
|
||||
.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,19 +23,53 @@ 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",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate stripPath requires a valid path
|
||||
if (input.stripPath && (!input.path || input.path === "/")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["stripPath"],
|
||||
message:
|
||||
"Strip path can only be enabled when a path other than '/' is specified",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate internalPath starts with /
|
||||
if (
|
||||
input.internalPath &&
|
||||
input.internalPath !== "/" &&
|
||||
!input.internalPath.startsWith("/")
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["internalPath"],
|
||||
message: "Internal path must start with '/'",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const domainCompose = z
|
||||
.object({
|
||||
host: z.string().min(1, { message: "Host is required" }),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
port: z
|
||||
.number()
|
||||
.min(1, { message: "Port must be at least 1" })
|
||||
.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 +80,35 @@ 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",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate stripPath requires a valid path
|
||||
if (input.stripPath && (!input.path || input.path === "/")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["stripPath"],
|
||||
message:
|
||||
"Strip path can only be enabled when a path other than '/' is specified",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate internalPath starts with /
|
||||
if (
|
||||
input.internalPath &&
|
||||
input.internalPath !== "/" &&
|
||||
!input.internalPath.startsWith("/")
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["internalPath"],
|
||||
message: "Internal path must start with '/'",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
export type TemplateProps = {
|
||||
projectName: string;
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
export type TemplateProps = {
|
||||
projectName: string;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
export type TemplateProps = {
|
||||
projectName: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
export type TemplateProps = {
|
||||
date: string;
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Preview,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
interface NotionMagicLinkEmailProps {
|
||||
loginCode?: string;
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Section,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
interface PlaidVerifyIdentityEmailProps {
|
||||
validationCode?: string;
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Section,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
const baseUrl = process.env.VERCEL_URL!;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
interface VercelInviteUserEmailProps {
|
||||
username?: string;
|
||||
|
||||
@@ -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";
|
||||
@@ -13,6 +10,7 @@ export * from "./services/mysql";
|
||||
export * from "./services/backup";
|
||||
export * from "./services/cluster";
|
||||
export * from "./services/settings";
|
||||
export * from "./services/volume-backups";
|
||||
export * from "./services/docker";
|
||||
export * from "./services/destination";
|
||||
export * from "./services/deployment";
|
||||
@@ -30,25 +28,33 @@ 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/gitea";
|
||||
export * from "./services/server";
|
||||
export * from "./services/schedule";
|
||||
export * from "./services/application";
|
||||
|
||||
export * from "./services/rollbacks";
|
||||
export * from "./utils/databases/rebuild";
|
||||
export * from "./setup/config-paths";
|
||||
export * from "./setup/postgres-setup";
|
||||
export * from "./setup/redis-setup";
|
||||
export * from "./setup/server-setup";
|
||||
export * from "./setup/monitoring-setup";
|
||||
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/providers/github";
|
||||
export * from "./utils/backups/index";
|
||||
export * from "./utils/backups/mariadb";
|
||||
export * from "./utils/backups/mongo";
|
||||
export * from "./utils/backups/mysql";
|
||||
export * from "./utils/backups/postgres";
|
||||
export * from "./utils/backups/utils";
|
||||
export * from "./utils/backups/web-server";
|
||||
export * from "./utils/backups/compose";
|
||||
export * from "./templates/processors";
|
||||
|
||||
export * from "./utils/notifications/build-error";
|
||||
export * from "./utils/notifications/build-success";
|
||||
@@ -56,6 +62,7 @@ export * from "./utils/notifications/database-backup";
|
||||
export * from "./utils/notifications/dokploy-restart";
|
||||
export * from "./utils/notifications/utils";
|
||||
export * from "./utils/notifications/docker-cleanup";
|
||||
export * from "./utils/notifications/server-threshold";
|
||||
|
||||
export * from "./utils/builders/index";
|
||||
export * from "./utils/builders/compose";
|
||||
@@ -70,6 +77,7 @@ export * from "./utils/builders/utils";
|
||||
export * from "./utils/cluster/upload";
|
||||
|
||||
export * from "./utils/docker/compose";
|
||||
export * from "./utils/docker/collision";
|
||||
export * from "./utils/docker/domain";
|
||||
export * from "./utils/docker/utils";
|
||||
export * from "./utils/docker/types";
|
||||
@@ -89,6 +97,7 @@ export * from "./utils/providers/docker";
|
||||
export * from "./utils/providers/git";
|
||||
export * from "./utils/providers/github";
|
||||
export * from "./utils/providers/gitlab";
|
||||
export * from "./utils/providers/gitea";
|
||||
export * from "./utils/providers/raw";
|
||||
|
||||
export * from "./utils/servers/remote-docker";
|
||||
@@ -102,11 +111,6 @@ export * from "./utils/traefik/security";
|
||||
export * from "./utils/traefik/types";
|
||||
export * from "./utils/traefik/web-server";
|
||||
|
||||
export * from "./wss/docker-container-logs";
|
||||
export * from "./wss/docker-container-terminal";
|
||||
export * from "./wss/docker-stats";
|
||||
export * from "./wss/listen-deployment";
|
||||
export * from "./wss/terminal";
|
||||
export * from "./wss/utils";
|
||||
|
||||
export * from "./utils/access-log/handler";
|
||||
@@ -114,8 +118,22 @@ export * from "./utils/access-log/types";
|
||||
export * from "./utils/access-log/utils";
|
||||
export * from "./constants/index";
|
||||
|
||||
export * from "./monitoring/utilts";
|
||||
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";
|
||||
|
||||
export * from "./utils/schedules/utils";
|
||||
export * from "./utils/schedules/index";
|
||||
export * from "./utils/volume-backups/index";
|
||||
|
||||
export * from "./lib/logger";
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { admin, apiKey, organization, twoFactor } from "better-auth/plugins";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { db } from "../db";
|
||||
import * as schema from "../db/schema";
|
||||
import { getUserByToken } from "../services/admin";
|
||||
import { updateUser } from "../services/user";
|
||||
import { sendEmail } from "../verification/send-verification-email";
|
||||
import { getPublicIpWithFallback } from "../wss/utils";
|
||||
|
||||
const { handler, api } = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema: schema,
|
||||
}),
|
||||
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,
|
||||
},
|
||||
},
|
||||
...(!IS_CLOUD && {
|
||||
async trustedOrigins() {
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (admin) {
|
||||
return [
|
||||
...(admin.user.serverIp
|
||||
? [`http://${admin.user.serverIp}:3000`]
|
||||
: []),
|
||||
...(admin.user.host ? [`https://${admin.user.host}`] : []),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
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: {
|
||||
before: async (_user, context) => {
|
||||
if (!IS_CLOUD) {
|
||||
const xDokployToken =
|
||||
context?.request?.headers?.get("x-dokploy-token");
|
||||
if (xDokployToken) {
|
||||
const user = await getUserByToken(xDokployToken);
|
||||
if (!user) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
});
|
||||
if (isAdminPresent) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Admin is already created",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
after: async (user) => {
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
});
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
await updateUser(user.id, {
|
||||
serverIp: await getPublicIpWithFallback(),
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 3,
|
||||
updateAge: 60 * 60 * 24,
|
||||
},
|
||||
user: {
|
||||
modelName: "users_temp",
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: "string",
|
||||
// required: true,
|
||||
input: false,
|
||||
},
|
||||
ownerId: {
|
||||
type: "string",
|
||||
// required: true,
|
||||
input: false,
|
||||
},
|
||||
allowImpersonation: {
|
||||
fieldName: "allowImpersonation",
|
||||
type: "boolean",
|
||||
defaultValue: 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://app.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>
|
||||
`,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
...(IS_CLOUD
|
||||
? [
|
||||
admin({
|
||||
adminUserIds: [process.env.USER_ADMIN_ID as string],
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
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: {
|
||||
userId: apiKeyRecord.user.id,
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import pino from "pino";
|
||||
|
||||
export const logger = pino({
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
levelFirst: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
+21
-95
@@ -1,10 +1,19 @@
|
||||
import { promises } from "node:fs";
|
||||
import type Dockerode from "dockerode";
|
||||
import osUtils from "node-os-utils";
|
||||
import { paths } from "../constants";
|
||||
|
||||
export interface Container {
|
||||
BlockIO: string;
|
||||
CPUPerc: string;
|
||||
Container: string;
|
||||
ID: string;
|
||||
MemPerc: string;
|
||||
MemUsage: string;
|
||||
Name: string;
|
||||
NetIO: string;
|
||||
}
|
||||
export const recordAdvancedStats = async (
|
||||
stats: Dockerode.ContainerStats,
|
||||
stats: Container,
|
||||
appName: string,
|
||||
) => {
|
||||
const { MONITORING_PATH } = paths();
|
||||
@@ -12,29 +21,20 @@ export const recordAdvancedStats = async (
|
||||
|
||||
await promises.mkdir(path, { recursive: true });
|
||||
|
||||
const cpuPercent = calculateCpuUsagePercent(
|
||||
stats.cpu_stats,
|
||||
stats.precpu_stats,
|
||||
);
|
||||
const memoryStats = calculateMemoryStats(stats.memory_stats);
|
||||
const blockIO = calculateBlockIO(stats.blkio_stats);
|
||||
const networkUsage = calculateNetworkUsage(stats.networks);
|
||||
|
||||
await updateStatsFile(appName, "cpu", cpuPercent);
|
||||
await updateStatsFile(appName, "cpu", stats.CPUPerc);
|
||||
await updateStatsFile(appName, "memory", {
|
||||
used: memoryStats.used,
|
||||
free: memoryStats.free,
|
||||
usedPercentage: memoryStats.usedPercentage,
|
||||
total: memoryStats.total,
|
||||
used: stats.MemUsage.split(" ")[0],
|
||||
total: stats.MemUsage.split(" ")[2],
|
||||
});
|
||||
|
||||
await updateStatsFile(appName, "block", {
|
||||
readMb: blockIO.readMb,
|
||||
writeMb: blockIO.writeMb,
|
||||
readMb: stats.BlockIO.split(" ")[0],
|
||||
writeMb: stats.BlockIO.split(" ")[2],
|
||||
});
|
||||
|
||||
await updateStatsFile(appName, "network", {
|
||||
inputMb: networkUsage.inputMb,
|
||||
outputMb: networkUsage.outputMb,
|
||||
inputMb: stats.NetIO.split(" ")[0],
|
||||
outputMb: stats.NetIO.split(" ")[2],
|
||||
});
|
||||
|
||||
if (appName === "dokploy") {
|
||||
@@ -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 {
|
||||
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 {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -122,77 +122,3 @@ export const getLastAdvancedStatsFile = async (appName: string) => {
|
||||
block: await readLastValueStatsFile(appName, "block"),
|
||||
};
|
||||
};
|
||||
|
||||
const calculateCpuUsagePercent = (
|
||||
cpu_stats: Dockerode.ContainerStats["cpu_stats"],
|
||||
precpu_stats: Dockerode.ContainerStats["precpu_stats"],
|
||||
) => {
|
||||
const cpuDelta =
|
||||
cpu_stats.cpu_usage.total_usage - precpu_stats.cpu_usage.total_usage;
|
||||
const systemDelta =
|
||||
cpu_stats.system_cpu_usage - precpu_stats.system_cpu_usage;
|
||||
|
||||
const numberCpus =
|
||||
cpu_stats.online_cpus ||
|
||||
(cpu_stats.cpu_usage.percpu_usage
|
||||
? cpu_stats.cpu_usage.percpu_usage.length
|
||||
: 1);
|
||||
|
||||
if (systemDelta > 0 && cpuDelta > 0) {
|
||||
return (cpuDelta / systemDelta) * numberCpus * 100.0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const calculateMemoryStats = (
|
||||
memory_stats: Dockerode.ContainerStats["memory_stats"],
|
||||
) => {
|
||||
const usedMemory = memory_stats.usage - (memory_stats.stats.cache || 0);
|
||||
const availableMemory = memory_stats.limit;
|
||||
const memoryUsedPercentage = (usedMemory / availableMemory) * 100.0;
|
||||
|
||||
return {
|
||||
used: usedMemory,
|
||||
free: availableMemory - usedMemory,
|
||||
usedPercentage: memoryUsedPercentage,
|
||||
total: availableMemory,
|
||||
};
|
||||
};
|
||||
const calculateBlockIO = (
|
||||
blkio_stats: Dockerode.ContainerStats["blkio_stats"],
|
||||
) => {
|
||||
let readIO = 0;
|
||||
let writeIO = 0;
|
||||
if (blkio_stats?.io_service_bytes_recursive) {
|
||||
for (const io of blkio_stats.io_service_bytes_recursive) {
|
||||
if (io.op === "read") {
|
||||
readIO += io.value;
|
||||
} else if (io.op === "write") {
|
||||
writeIO += io.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
readMb: readIO / (1024 * 1024),
|
||||
writeMb: writeIO / (1024 * 1024),
|
||||
};
|
||||
};
|
||||
|
||||
const calculateNetworkUsage = (
|
||||
networks: Dockerode.ContainerStats["networks"],
|
||||
) => {
|
||||
let totalRx = 0;
|
||||
let totalTx = 0;
|
||||
|
||||
const stats = Object.keys(networks);
|
||||
|
||||
for (const interfaceName of stats) {
|
||||
const net = networks[interfaceName];
|
||||
totalRx += net?.rx_bytes || 0;
|
||||
totalTx += net?.tx_bytes || 0;
|
||||
}
|
||||
return {
|
||||
inputMb: totalRx / (1024 * 1024),
|
||||
outputMb: totalTx / (1024 * 1024),
|
||||
};
|
||||
};
|
||||
@@ -1,105 +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 to create 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 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),
|
||||
});
|
||||
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({});
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
@@ -110,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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -127,16 +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))
|
||||
.delete(users_temp)
|
||||
.where(eq(users_temp.id, userId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
};
|
||||
@@ -147,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}`;
|
||||
};
|
||||
|
||||
@@ -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 { findOrganizationById } from "./admin";
|
||||
import { findServerById } from "./server";
|
||||
|
||||
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., "[email protected]" 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;
|
||||
}
|
||||
};
|
||||
@@ -3,10 +3,9 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateApplication,
|
||||
applications,
|
||||
buildAppName,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { getAdvancedStats } from "@dokploy/server/monitoring/utilts";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { getAdvancedStats } from "@dokploy/server/monitoring/utils";
|
||||
import {
|
||||
buildApplication,
|
||||
getBuildCommand,
|
||||
@@ -28,7 +27,10 @@ import {
|
||||
getCustomGitCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
authGithub,
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/github";
|
||||
@@ -46,34 +48,32 @@ import {
|
||||
createDeploymentPreview,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { type Domain, getDomainHost } from "./domain";
|
||||
import {
|
||||
createPreviewDeploymentComment,
|
||||
getIssueComment,
|
||||
issueCommentExists,
|
||||
updateIssueComment,
|
||||
} from "./github";
|
||||
import { type Domain, getDomainHost } from "./domain";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { createRollback } from "./rollbacks";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
input: typeof apiCreateApplication._type,
|
||||
) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("app", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Application with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Application with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
@@ -81,6 +81,7 @@ export const createApplication = async (
|
||||
.insert(applications)
|
||||
.values({
|
||||
...input,
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -88,7 +89,7 @@ export const createApplication = async (
|
||||
if (!newApplication) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the application",
|
||||
message: "Error creating the application",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,6 +116,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
gitlab: true,
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
previewDeployments: true,
|
||||
},
|
||||
@@ -140,10 +142,11 @@ export const updateApplication = async (
|
||||
applicationId: string,
|
||||
applicationData: Partial<Application>,
|
||||
) => {
|
||||
const { appName, ...rest } = applicationData;
|
||||
const application = await db
|
||||
.update(applications)
|
||||
.set({
|
||||
...applicationData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(applications.applicationId, applicationId))
|
||||
.returning();
|
||||
@@ -176,6 +179,7 @@ export const deployApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
@@ -193,6 +197,9 @@ export const deployApplication = async ({
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
@@ -208,24 +215,37 @@ export const deployApplication = async ({
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
organizationId: application.project.organizationId,
|
||||
domains: application.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
|
||||
await sendBuildErrorNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
errorMessage: error?.message || "Error building",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
organizationId: application.project.organizationId,
|
||||
});
|
||||
|
||||
throw error;
|
||||
@@ -244,6 +264,7 @@ export const rebuildApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
@@ -285,6 +306,7 @@ export const deployRemoteApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
@@ -308,6 +330,8 @@ export const deployRemoteApplication = async ({
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(application, deployment.logPath);
|
||||
} else if (application.sourceType === "git") {
|
||||
command += await getCustomGitCloneCommand(
|
||||
application,
|
||||
@@ -327,16 +351,29 @@ export const deployRemoteApplication = async ({
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
organizationId: application.project.organizationId,
|
||||
domains: application.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const encodedContent = encodeBase64(error?.message);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
const encodedContent = encodeBase64(errorMessage);
|
||||
|
||||
await execAsyncRemote(
|
||||
application.serverId,
|
||||
@@ -348,24 +385,16 @@ export const deployRemoteApplication = async ({
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
|
||||
await sendBuildErrorNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
errorMessage: `Please check the logs for details: ${errorMessage}`,
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
organizationId: application.project.organizationId,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Error on ",
|
||||
application.buildType,
|
||||
"/",
|
||||
application.sourceType,
|
||||
error,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -384,6 +413,7 @@ export const deployPreviewApplication = async ({
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
@@ -437,7 +467,7 @@ export const deployPreviewApplication = async ({
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = application.previewEnv;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
if (application.sourceType === "github") {
|
||||
@@ -449,7 +479,6 @@ export const deployPreviewApplication = async ({
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
// 4eef09efc46009187d668cf1c25f768d0bde4f91
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
"success",
|
||||
@@ -491,6 +520,7 @@ export const deployRemotePreviewApplication = async ({
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
@@ -544,7 +574,7 @@ export const deployRemotePreviewApplication = async ({
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = application.previewEnv;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
if (application.serverId) {
|
||||
@@ -552,6 +582,8 @@ export const deployRemotePreviewApplication = async ({
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
appName: previewDeployment.appName,
|
||||
branch: previewDeployment.branch,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
@@ -601,6 +633,7 @@ export const rebuildRemoteApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
|
||||
@@ -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/terminal";
|
||||
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 to create 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 to create 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;
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { eq } from "drizzle-orm";
|
||||
export type Backup = typeof backups.$inferSelect;
|
||||
|
||||
export type BackupSchedule = Awaited<ReturnType<typeof findBackupById>>;
|
||||
|
||||
export type BackupScheduleList = Awaited<ReturnType<typeof findBackupsByDbId>>;
|
||||
export const createBackup = async (input: typeof apiCreateBackup._type) => {
|
||||
const newBackup = await db
|
||||
.insert(backups)
|
||||
@@ -19,7 +19,7 @@ export const createBackup = async (input: typeof apiCreateBackup._type) => {
|
||||
if (!newBackup) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the Backup",
|
||||
message: "Error creating the Backup",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export const findBackupById = async (backupId: string) => {
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
destination: true,
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
if (!backup) {
|
||||
@@ -69,3 +70,20 @@ export const removeBackupById = async (backupId: string) => {
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findBackupsByDbId = async (
|
||||
id: string,
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo",
|
||||
) => {
|
||||
const result = await db.query.backups.findMany({
|
||||
where: eq(backups[`${type}Id`], id),
|
||||
with: {
|
||||
postgres: true,
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
return result || [];
|
||||
};
|
||||
|
||||
@@ -12,15 +12,17 @@ export type Bitbucket = typeof bitbucket.$inferSelect;
|
||||
|
||||
export const createBitbucket = async (
|
||||
input: typeof apiCreateBitbucket._type,
|
||||
adminId: string,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "bitbucket",
|
||||
adminId: adminId,
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
@@ -28,7 +30,7 @@ export const createBitbucket = async (
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the git provider",
|
||||
message: "Error creating the Bitbucket provider",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,12 +76,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();
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
// CDN Provider Interface
|
||||
export interface CDNProvider {
|
||||
name: string;
|
||||
displayName: string;
|
||||
checkIp: (ip: string) => boolean;
|
||||
warningMessage: string;
|
||||
}
|
||||
|
||||
const isIPInCIDR = (ip: string, cidr: string): boolean => {
|
||||
const [network, prefixLength] = cidr.split("/");
|
||||
if (!network || !prefixLength) return false;
|
||||
const prefix = Number.parseInt(prefixLength, 10);
|
||||
|
||||
// Convert IP addresses to 32-bit integers
|
||||
const ipToInt = (ipStr: string): number => {
|
||||
return (
|
||||
ipStr
|
||||
.split(".")
|
||||
.reduce((acc, octet) => (acc << 8) + Number.parseInt(octet, 10), 0) >>>
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const ipInt = ipToInt(ip);
|
||||
const networkInt = ipToInt(network);
|
||||
const mask = (0xffffffff << (32 - prefix)) >>> 0;
|
||||
|
||||
return (ipInt & mask) === (networkInt & mask);
|
||||
};
|
||||
|
||||
// Cloudflare IP ranges
|
||||
// https://www.cloudflare.com/ips-v4
|
||||
const CLOUDFLARE_IP_RANGES = [
|
||||
"173.245.48.0/20",
|
||||
"103.21.244.0/22",
|
||||
"103.22.200.0/22",
|
||||
"103.31.4.0/22",
|
||||
"141.101.64.0/18",
|
||||
"108.162.192.0/18",
|
||||
"190.93.240.0/20",
|
||||
"188.114.96.0/20",
|
||||
"197.234.240.0/22",
|
||||
"198.41.128.0/17",
|
||||
"162.158.0.0/15",
|
||||
"104.16.0.0/13",
|
||||
"104.24.0.0/14",
|
||||
"172.64.0.0/13",
|
||||
"131.0.72.0/22",
|
||||
];
|
||||
|
||||
// Fastly IP ranges
|
||||
// https://api.fastly.com/public-ip-list
|
||||
const FASTLY_IP_RANGES = [
|
||||
"23.235.32.0/20",
|
||||
"43.249.72.0/22",
|
||||
"103.244.50.0/24",
|
||||
"103.245.222.0/23",
|
||||
"103.245.224.0/24",
|
||||
"104.156.80.0/20",
|
||||
"140.248.64.0/18",
|
||||
"140.248.128.0/17",
|
||||
"146.75.0.0/17",
|
||||
"151.101.0.0/16",
|
||||
"157.52.64.0/18",
|
||||
"167.82.0.0/17",
|
||||
"167.82.128.0/20",
|
||||
"167.82.160.0/20",
|
||||
"167.82.224.0/20",
|
||||
"172.111.64.0/18",
|
||||
"185.31.16.0/22",
|
||||
"199.27.72.0/21",
|
||||
"199.232.0.0/16",
|
||||
];
|
||||
|
||||
// Bunny CDN IP addresses
|
||||
// https://bunnycdn.com/api/system/edgeserverlist
|
||||
const BUNNY_CDN_IPS = new Set([
|
||||
"89.187.188.227",
|
||||
"89.187.188.228",
|
||||
"139.180.134.196",
|
||||
"89.38.96.158",
|
||||
"89.187.162.249",
|
||||
"89.187.162.242",
|
||||
"185.102.217.65",
|
||||
"185.93.1.243",
|
||||
"156.146.40.49",
|
||||
"185.59.220.199",
|
||||
"185.59.220.198",
|
||||
"195.181.166.158",
|
||||
"185.180.12.68",
|
||||
"138.199.24.209",
|
||||
"138.199.24.211",
|
||||
"79.127.216.111",
|
||||
"79.127.216.112",
|
||||
"89.187.169.47",
|
||||
"138.199.24.218",
|
||||
"185.40.106.117",
|
||||
"200.25.45.4",
|
||||
"200.25.57.5",
|
||||
"193.162.131.1",
|
||||
"200.25.11.8",
|
||||
"200.25.53.5",
|
||||
"200.25.13.98",
|
||||
"41.242.2.18",
|
||||
"200.25.62.5",
|
||||
"200.25.38.69",
|
||||
"200.25.42.70",
|
||||
"200.25.36.166",
|
||||
"195.206.229.106",
|
||||
"194.242.11.186",
|
||||
"185.164.35.8",
|
||||
"94.20.154.22",
|
||||
"185.93.1.244",
|
||||
"156.59.145.154",
|
||||
"143.244.49.177",
|
||||
"138.199.46.66",
|
||||
"138.199.37.227",
|
||||
"138.199.37.231",
|
||||
"138.199.37.230",
|
||||
"138.199.37.229",
|
||||
"138.199.46.69",
|
||||
"138.199.46.68",
|
||||
"138.199.46.67",
|
||||
"185.93.1.246",
|
||||
"138.199.37.232",
|
||||
"195.181.163.196",
|
||||
"107.182.163.162",
|
||||
"195.181.163.195",
|
||||
"84.17.46.53",
|
||||
"212.102.40.114",
|
||||
"84.17.46.54",
|
||||
"138.199.40.58",
|
||||
"143.244.38.134",
|
||||
"143.244.38.136",
|
||||
"185.152.64.17",
|
||||
"84.17.59.115",
|
||||
"89.187.165.194",
|
||||
"138.199.15.193",
|
||||
"89.35.237.170",
|
||||
"37.19.216.130",
|
||||
"185.93.1.247",
|
||||
"185.93.3.244",
|
||||
"143.244.49.179",
|
||||
"143.244.49.180",
|
||||
"138.199.9.104",
|
||||
"185.152.66.243",
|
||||
"143.244.49.178",
|
||||
"169.150.221.147",
|
||||
"200.25.18.73",
|
||||
"84.17.63.178",
|
||||
"200.25.32.131",
|
||||
"37.19.207.34",
|
||||
"192.189.65.146",
|
||||
"143.244.45.177",
|
||||
"185.93.1.249",
|
||||
"185.93.1.250",
|
||||
"169.150.215.115",
|
||||
"209.177.87.197",
|
||||
"156.146.56.162",
|
||||
"156.146.56.161",
|
||||
"185.93.2.246",
|
||||
"185.93.2.245",
|
||||
"212.102.40.113",
|
||||
"185.93.2.244",
|
||||
"143.244.50.82",
|
||||
"143.244.50.83",
|
||||
"156.146.56.163",
|
||||
"129.227.217.178",
|
||||
"129.227.217.179",
|
||||
"200.25.69.94",
|
||||
"128.1.52.179",
|
||||
"200.25.16.103",
|
||||
"15.235.54.226",
|
||||
"102.67.138.155",
|
||||
"156.146.43.65",
|
||||
"195.181.163.203",
|
||||
"195.181.163.202",
|
||||
"156.146.56.169",
|
||||
"156.146.56.170",
|
||||
"156.146.56.166",
|
||||
"156.146.56.171",
|
||||
"169.150.207.210",
|
||||
"156.146.56.167",
|
||||
"143.244.50.84",
|
||||
"143.244.50.85",
|
||||
"143.244.50.86",
|
||||
"143.244.50.87",
|
||||
"156.146.56.168",
|
||||
"169.150.207.211",
|
||||
"212.102.50.59",
|
||||
"146.185.248.15",
|
||||
"143.244.50.90",
|
||||
"143.244.50.91",
|
||||
"143.244.50.88",
|
||||
"143.244.50.209",
|
||||
"143.244.50.213",
|
||||
"143.244.50.214",
|
||||
"143.244.49.183",
|
||||
"143.244.50.89",
|
||||
"143.244.50.210",
|
||||
"143.244.50.211",
|
||||
"143.244.50.212",
|
||||
"5.42.206.66",
|
||||
"94.46.27.186",
|
||||
"169.150.207.213",
|
||||
"169.150.207.214",
|
||||
"169.150.207.215",
|
||||
"169.150.207.212",
|
||||
"169.150.219.114",
|
||||
"169.150.202.210",
|
||||
"169.150.242.193",
|
||||
"185.93.1.251",
|
||||
"169.150.207.216",
|
||||
"169.150.207.217",
|
||||
"169.150.238.19",
|
||||
"102.219.126.20",
|
||||
"156.59.66.182",
|
||||
"122.10.251.130",
|
||||
"185.24.11.18",
|
||||
"138.199.36.7",
|
||||
"138.199.36.8",
|
||||
"138.199.36.9",
|
||||
"138.199.36.10",
|
||||
"138.199.36.11",
|
||||
"138.199.37.225",
|
||||
"84.17.46.49",
|
||||
"84.17.37.217",
|
||||
"169.150.225.35",
|
||||
"169.150.225.36",
|
||||
"169.150.225.37",
|
||||
"169.150.225.38",
|
||||
"169.150.225.39",
|
||||
"169.150.225.34",
|
||||
"169.150.236.97",
|
||||
"169.150.236.98",
|
||||
"169.150.236.99",
|
||||
"169.150.236.100",
|
||||
"93.189.63.149",
|
||||
"143.244.56.49",
|
||||
"143.244.56.50",
|
||||
"143.244.56.51",
|
||||
"169.150.247.40",
|
||||
"169.150.247.33",
|
||||
"169.150.247.34",
|
||||
"169.150.247.35",
|
||||
"169.150.247.36",
|
||||
"169.150.247.37",
|
||||
"169.150.247.38",
|
||||
"169.150.247.39",
|
||||
"38.142.94.218",
|
||||
"87.249.137.52",
|
||||
"38.104.169.186",
|
||||
"66.181.163.74",
|
||||
"84.17.38.227",
|
||||
"84.17.38.228",
|
||||
"84.17.38.229",
|
||||
"84.17.38.230",
|
||||
"84.17.38.231",
|
||||
"84.17.38.232",
|
||||
"169.150.225.41",
|
||||
"169.150.225.42",
|
||||
"169.150.249.162",
|
||||
"169.150.249.163",
|
||||
"169.150.249.164",
|
||||
"169.150.249.165",
|
||||
"169.150.249.166",
|
||||
"169.150.249.167",
|
||||
"169.150.249.168",
|
||||
"169.150.249.169",
|
||||
"185.131.64.124",
|
||||
"103.112.0.22",
|
||||
"37.236.234.2",
|
||||
"169.150.252.209",
|
||||
"212.102.46.118",
|
||||
"192.169.120.162",
|
||||
"93.180.217.214",
|
||||
"37.19.203.178",
|
||||
"107.155.47.146",
|
||||
"193.201.190.174",
|
||||
"156.59.95.218",
|
||||
"213.170.143.139",
|
||||
"129.227.186.154",
|
||||
"195.238.127.98",
|
||||
"200.25.22.6",
|
||||
"204.16.244.92",
|
||||
"200.25.70.101",
|
||||
"200.25.66.100",
|
||||
"139.180.209.182",
|
||||
"103.108.231.41",
|
||||
"103.108.229.5",
|
||||
"103.216.220.9",
|
||||
"169.150.225.40",
|
||||
"212.102.50.49",
|
||||
"212.102.50.52",
|
||||
"109.61.83.242",
|
||||
"109.61.83.243",
|
||||
"212.102.50.50",
|
||||
"169.150.225.43",
|
||||
"45.125.247.57",
|
||||
"103.235.199.170",
|
||||
"128.1.35.170",
|
||||
"38.32.110.58",
|
||||
"169.150.220.228",
|
||||
"169.150.220.229",
|
||||
"169.150.220.230",
|
||||
"169.150.220.231",
|
||||
"138.199.4.179",
|
||||
"207.211.214.145",
|
||||
"109.61.86.193",
|
||||
"103.214.20.95",
|
||||
"178.175.134.51",
|
||||
"138.199.4.178",
|
||||
"172.255.253.140",
|
||||
"185.24.11.19",
|
||||
"109.61.83.244",
|
||||
"109.61.83.245",
|
||||
"84.17.38.250",
|
||||
"84.17.38.251",
|
||||
"146.59.69.202",
|
||||
"146.70.80.218",
|
||||
"200.25.80.74",
|
||||
"79.127.213.214",
|
||||
"79.127.213.215",
|
||||
"79.127.213.216",
|
||||
"79.127.213.217",
|
||||
"195.69.140.112",
|
||||
"109.61.83.247",
|
||||
"109.61.83.246",
|
||||
"185.93.2.248",
|
||||
"109.61.83.249",
|
||||
"109.61.83.250",
|
||||
"109.61.83.251",
|
||||
"46.199.75.115",
|
||||
"141.164.35.160",
|
||||
"109.61.83.97",
|
||||
"109.61.83.98",
|
||||
"109.61.83.99",
|
||||
"129.227.179.18",
|
||||
"185.180.14.250",
|
||||
"152.89.160.26",
|
||||
"5.189.202.62",
|
||||
"98.98.242.142",
|
||||
"156.59.92.126",
|
||||
"84.17.59.117",
|
||||
"79.127.216.66",
|
||||
"79.127.204.113",
|
||||
"79.127.237.132",
|
||||
"169.150.236.104",
|
||||
"169.150.236.105",
|
||||
"37.27.135.61",
|
||||
"158.51.123.205",
|
||||
"156.146.43.70",
|
||||
"156.146.43.71",
|
||||
"156.146.43.72",
|
||||
"180.149.231.175",
|
||||
"185.93.2.243",
|
||||
"143.244.56.52",
|
||||
"143.244.56.53",
|
||||
"143.244.56.54",
|
||||
"143.244.56.55",
|
||||
"143.244.56.56",
|
||||
"143.244.56.57",
|
||||
"143.244.56.58",
|
||||
"144.76.236.44",
|
||||
"88.198.57.50",
|
||||
"78.46.69.199",
|
||||
"136.243.16.49",
|
||||
"138.201.86.122",
|
||||
"136.243.42.90",
|
||||
"88.99.95.221",
|
||||
"178.63.2.112",
|
||||
"5.9.98.45",
|
||||
"136.243.42.10",
|
||||
"169.150.236.106",
|
||||
"169.150.236.107",
|
||||
"185.93.1.242",
|
||||
"185.93.1.245",
|
||||
"143.244.60.193",
|
||||
"195.181.163.194",
|
||||
"79.127.188.193",
|
||||
"79.127.188.196",
|
||||
"79.127.188.194",
|
||||
"79.127.188.195",
|
||||
"104.166.144.106",
|
||||
"156.59.126.78",
|
||||
"185.135.85.154",
|
||||
"38.54.5.37",
|
||||
"38.54.3.92",
|
||||
"185.165.170.74",
|
||||
"207.121.80.118",
|
||||
"207.121.46.228",
|
||||
"207.121.46.236",
|
||||
"207.121.46.244",
|
||||
"207.121.46.252",
|
||||
"216.202.235.164",
|
||||
"207.121.46.220",
|
||||
"207.121.75.132",
|
||||
"207.121.80.12",
|
||||
"207.121.80.172",
|
||||
"207.121.90.60",
|
||||
"207.121.90.68",
|
||||
"207.121.97.204",
|
||||
"207.121.90.252",
|
||||
"207.121.97.236",
|
||||
"207.121.99.12",
|
||||
"138.199.24.219",
|
||||
"185.93.2.251",
|
||||
"138.199.46.65",
|
||||
"207.121.41.196",
|
||||
"207.121.99.20",
|
||||
"207.121.99.36",
|
||||
"207.121.99.44",
|
||||
"207.121.99.52",
|
||||
"207.121.99.60",
|
||||
"207.121.23.68",
|
||||
"207.121.23.124",
|
||||
"207.121.23.244",
|
||||
"207.121.23.180",
|
||||
"207.121.23.188",
|
||||
"207.121.23.196",
|
||||
"207.121.23.204",
|
||||
"207.121.24.52",
|
||||
"207.121.24.60",
|
||||
"207.121.24.68",
|
||||
"207.121.24.76",
|
||||
"207.121.24.92",
|
||||
"207.121.24.100",
|
||||
"207.121.24.108",
|
||||
"207.121.24.116",
|
||||
"154.95.86.76",
|
||||
"5.9.99.73",
|
||||
"78.46.92.118",
|
||||
"144.76.65.213",
|
||||
"78.46.156.89",
|
||||
"88.198.9.155",
|
||||
"144.76.79.22",
|
||||
"103.1.215.93",
|
||||
"103.137.12.33",
|
||||
"103.107.196.31",
|
||||
"116.90.72.155",
|
||||
"103.137.14.5",
|
||||
"116.90.75.65",
|
||||
"37.19.207.37",
|
||||
"208.83.234.224",
|
||||
"116.202.155.146",
|
||||
"116.202.193.178",
|
||||
"116.202.224.168",
|
||||
"188.40.126.227",
|
||||
"88.99.26.189",
|
||||
"168.119.39.238",
|
||||
"88.99.26.97",
|
||||
"168.119.12.188",
|
||||
"176.9.139.55",
|
||||
"142.132.223.79",
|
||||
"142.132.223.80",
|
||||
"142.132.223.81",
|
||||
"46.4.116.17",
|
||||
"46.4.119.81",
|
||||
"167.235.114.167",
|
||||
"159.69.68.171",
|
||||
"178.63.21.52",
|
||||
"46.4.120.152",
|
||||
"116.202.80.247",
|
||||
"5.9.71.119",
|
||||
"195.201.11.156",
|
||||
"78.46.123.17",
|
||||
"46.4.113.143",
|
||||
"136.243.2.236",
|
||||
"195.201.81.217",
|
||||
"148.251.42.123",
|
||||
"94.130.68.122",
|
||||
"88.198.22.103",
|
||||
"46.4.102.90",
|
||||
"157.90.180.205",
|
||||
"162.55.135.11",
|
||||
"195.201.109.59",
|
||||
"148.251.41.244",
|
||||
"116.202.235.16",
|
||||
"128.140.70.141",
|
||||
"78.46.74.86",
|
||||
"78.46.74.85",
|
||||
"178.63.41.242",
|
||||
"178.63.41.247",
|
||||
"178.63.41.234",
|
||||
"104.237.53.74",
|
||||
"104.237.54.154",
|
||||
"104.237.51.58",
|
||||
"64.185.235.90",
|
||||
"64.185.234.114",
|
||||
"64.185.232.194",
|
||||
"64.185.232.178",
|
||||
"64.185.232.82",
|
||||
"103.60.15.169",
|
||||
"103.60.15.170",
|
||||
"103.60.15.171",
|
||||
"103.60.15.172",
|
||||
"103.60.15.173",
|
||||
"103.60.15.162",
|
||||
"103.60.15.163",
|
||||
"103.60.15.164",
|
||||
"103.60.15.165",
|
||||
"103.60.15.166",
|
||||
"103.60.15.167",
|
||||
"103.60.15.168",
|
||||
"109.248.43.116",
|
||||
"109.248.43.117",
|
||||
"109.248.43.162",
|
||||
"109.248.43.163",
|
||||
"109.248.43.164",
|
||||
"109.248.43.165",
|
||||
"49.12.71.27",
|
||||
"49.12.0.158",
|
||||
"78.47.94.156",
|
||||
"109.248.43.159",
|
||||
"109.248.43.160",
|
||||
"109.248.43.208",
|
||||
"109.248.43.179",
|
||||
"109.248.43.232",
|
||||
"109.248.43.231",
|
||||
"109.248.43.241",
|
||||
"109.248.43.236",
|
||||
"109.248.43.240",
|
||||
"109.248.43.103",
|
||||
"116.202.118.194",
|
||||
"116.202.80.29",
|
||||
"159.69.57.80",
|
||||
"139.180.129.216",
|
||||
"139.99.174.7",
|
||||
"89.187.169.18",
|
||||
"89.187.179.7",
|
||||
"143.244.62.213",
|
||||
"185.93.3.246",
|
||||
"195.181.163.198",
|
||||
"185.152.64.19",
|
||||
"84.17.37.211",
|
||||
"212.102.50.54",
|
||||
"212.102.46.115",
|
||||
"143.244.38.135",
|
||||
"169.150.238.21",
|
||||
"169.150.207.51",
|
||||
"169.150.207.49",
|
||||
"84.17.38.226",
|
||||
"84.17.38.225",
|
||||
"37.19.222.248",
|
||||
"37.19.222.249",
|
||||
"169.150.247.139",
|
||||
"169.150.247.177",
|
||||
"169.150.247.178",
|
||||
"169.150.213.49",
|
||||
"212.102.46.119",
|
||||
"84.17.38.234",
|
||||
"84.17.38.233",
|
||||
"169.150.247.179",
|
||||
"169.150.247.180",
|
||||
"169.150.247.181",
|
||||
"169.150.247.182",
|
||||
"169.150.247.183",
|
||||
"169.150.247.138",
|
||||
"169.150.247.184",
|
||||
"169.150.247.185",
|
||||
"156.146.58.83",
|
||||
"212.102.43.88",
|
||||
"89.187.169.26",
|
||||
"109.61.89.57",
|
||||
"109.61.89.58",
|
||||
"109.61.83.241",
|
||||
"84.17.38.243",
|
||||
"84.17.38.244",
|
||||
"84.17.38.246",
|
||||
"84.17.38.247",
|
||||
"84.17.38.245",
|
||||
"143.244.38.129",
|
||||
"84.17.38.248",
|
||||
"89.187.176.34",
|
||||
"185.152.64.23",
|
||||
"79.127.213.209",
|
||||
"79.127.213.210",
|
||||
"84.17.37.209",
|
||||
"156.146.43.68",
|
||||
"185.93.3.243",
|
||||
"79.127.219.198",
|
||||
"138.199.33.57",
|
||||
"79.127.242.89",
|
||||
"138.199.4.136",
|
||||
"169.150.220.235",
|
||||
"138.199.4.129",
|
||||
"138.199.4.177",
|
||||
"37.19.222.34",
|
||||
"46.151.193.85",
|
||||
"212.104.158.17",
|
||||
"212.104.158.18",
|
||||
"212.104.158.19",
|
||||
"212.104.158.20",
|
||||
"212.104.158.21",
|
||||
"212.104.158.22",
|
||||
"212.104.158.24",
|
||||
"212.104.158.26",
|
||||
"79.127.237.134",
|
||||
"89.187.184.177",
|
||||
"89.187.184.179",
|
||||
"89.187.184.173",
|
||||
"89.187.184.178",
|
||||
"89.187.184.176",
|
||||
]);
|
||||
|
||||
const CDN_PROVIDERS: CDNProvider[] = [
|
||||
{
|
||||
name: "cloudflare",
|
||||
displayName: "Cloudflare",
|
||||
checkIp: (ip: string) =>
|
||||
CLOUDFLARE_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
|
||||
},
|
||||
{
|
||||
name: "bunnycdn",
|
||||
displayName: "Bunny CDN",
|
||||
checkIp: (ip: string) => BUNNY_CDN_IPS.has(ip),
|
||||
warningMessage:
|
||||
"Domain is behind Bunny CDN - actual IP is masked by CDN proxy",
|
||||
},
|
||||
{
|
||||
name: "fastly",
|
||||
displayName: "Fastly",
|
||||
checkIp: (ip: string) =>
|
||||
FASTLY_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Fastly - actual IP is masked by CDN proxy",
|
||||
},
|
||||
];
|
||||
|
||||
export const detectCDNProvider = (ip: string): CDNProvider | null => {
|
||||
return CDN_PROVIDERS.find((provider) => provider.checkIp(ip)) || null;
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import {
|
||||
type apiCreateCompose,
|
||||
buildAppName,
|
||||
cleanAppName,
|
||||
compose,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import {
|
||||
buildCompose,
|
||||
getBuildComposeCommand,
|
||||
@@ -30,6 +33,10 @@ import {
|
||||
cloneGitRepository,
|
||||
getCustomGitCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/git";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
cloneGithubRepository,
|
||||
getGithubCloneCommand,
|
||||
@@ -52,24 +59,22 @@ import { validUniqueServerAppName } from "./project";
|
||||
export type Compose = typeof compose.$inferSelect;
|
||||
|
||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("compose", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await db
|
||||
.insert(compose)
|
||||
.values({
|
||||
...input,
|
||||
composeFile: "",
|
||||
composeFile: input.composeFile || "",
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -87,8 +92,9 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
export const createComposeByTemplate = async (
|
||||
input: typeof compose.$inferInsert,
|
||||
) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = cleanAppName(input.appName);
|
||||
if (appName) {
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
@@ -101,6 +107,7 @@ export const createComposeByTemplate = async (
|
||||
.insert(compose)
|
||||
.values({
|
||||
...input,
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -126,7 +133,14 @@ export const findComposeById = async (composeId: string) => {
|
||||
github: true,
|
||||
gitlab: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
@@ -184,10 +198,11 @@ export const updateCompose = async (
|
||||
composeId: string,
|
||||
composeData: Partial<Compose>,
|
||||
) => {
|
||||
const { appName, ...rest } = composeData;
|
||||
const composeResult = await db
|
||||
.update(compose)
|
||||
.set({
|
||||
...composeData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(compose.composeId, composeId))
|
||||
.returning();
|
||||
@@ -205,7 +220,10 @@ export const deployCompose = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.projectId
|
||||
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -225,6 +243,8 @@ export const deployCompose = async ({
|
||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
}
|
||||
@@ -239,7 +259,8 @@ export const deployCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
organizationId: compose.project.organizationId,
|
||||
domains: compose.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
@@ -251,9 +272,9 @@ export const deployCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
errorMessage: error?.message || "Error building",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
organizationId: compose.project.organizationId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -269,6 +290,7 @@ export const rebuildCompose = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -276,11 +298,10 @@ export const rebuildCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
if (compose.serverId) {
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
} else {
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
@@ -307,7 +328,10 @@ export const deployRemoteCompose = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.projectId
|
||||
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -342,8 +366,15 @@ export const deployRemoteCompose = async ({
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
console.log(command);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
command += getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
@@ -360,7 +391,8 @@ export const deployRemoteCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
organizationId: compose.project.organizationId,
|
||||
domains: compose.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
@@ -382,9 +414,9 @@ export const deployRemoteCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
errorMessage: error?.message || "Error building",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
organizationId: compose.project.organizationId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -400,6 +432,7 @@ export const rebuildRemoteCompose = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -407,6 +440,10 @@ export const rebuildRemoteCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
if (compose.sourceType === "raw") {
|
||||
const command = getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
}
|
||||
if (compose.serverId) {
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
}
|
||||
@@ -436,13 +473,19 @@ export const rebuildRemoteCompose = async ({
|
||||
return true;
|
||||
};
|
||||
|
||||
export const removeCompose = async (compose: Compose) => {
|
||||
export const removeCompose = async (
|
||||
compose: Compose,
|
||||
deleteVolumes: boolean,
|
||||
) => {
|
||||
try {
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName);
|
||||
|
||||
if (compose.composeType === "stack") {
|
||||
const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
||||
const command = `
|
||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||
cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
||||
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
@@ -452,7 +495,12 @@ export const removeCompose = async (compose: Compose) => {
|
||||
cwd: projectPath,
|
||||
});
|
||||
} else {
|
||||
const command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
|
||||
const command = `
|
||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||
cd ${projectPath} && docker compose -p ${compose.appName} down ${
|
||||
deleteVolumes ? "--volumes" : ""
|
||||
} && rm -rf ${projectPath}`;
|
||||
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
@@ -472,15 +520,20 @@ export const startCompose = async (composeId: string) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
try {
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
const path =
|
||||
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
||||
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
|
||||
if (compose.composeType === "docker-compose") {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`,
|
||||
`cd ${projectPath} && ${baseCommand}`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} up -d`, {
|
||||
cwd: join(COMPOSE_PATH, compose.appName, "code"),
|
||||
await execAsync(baseCommand, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -506,7 +559,9 @@ export const stopCompose = async (composeId: string) => {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${
|
||||
compose.appName
|
||||
} stop`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
||||
@@ -515,6 +570,17 @@ export const stopCompose = async (composeId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (compose.composeType === "stack") {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`docker stack rm ${compose.appName}`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker stack rm ${compose.appName}`);
|
||||
}
|
||||
}
|
||||
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "idle",
|
||||
});
|
||||
|
||||
@@ -4,15 +4,18 @@ import { paths } from "@dokploy/server/constants";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateDeployment,
|
||||
type apiCreateDeploymentBackup,
|
||||
type apiCreateDeploymentCompose,
|
||||
type apiCreateDeploymentPreview,
|
||||
type apiCreateDeploymentSchedule,
|
||||
type apiCreateDeploymentServer,
|
||||
type apiCreateDeploymentVolumeBackup,
|
||||
deployments,
|
||||
} from "@dokploy/server/db/schema";
|
||||
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,
|
||||
@@ -22,28 +25,47 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
|
||||
import { type Server, findServerById } from "./server";
|
||||
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { findBackupById } from "./backup";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
type PreviewDeployment,
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { removeRollbackById } from "./rollbacks";
|
||||
import { findScheduleById } from "./schedule";
|
||||
import { findVolumeBackupById } from "./volume-backups";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
|
||||
export const findDeploymentById = async (applicationId: string) => {
|
||||
const application = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
export const findDeploymentById = async (deploymentId: string) => {
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.deploymentId, deploymentId),
|
||||
with: {
|
||||
application: true,
|
||||
schedule: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
if (!deployment) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Deployment not found",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
return deployment;
|
||||
};
|
||||
|
||||
export const findDeploymentByApplicationId = async (applicationId: string) => {
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Deployment not found",
|
||||
});
|
||||
}
|
||||
return deployment;
|
||||
};
|
||||
|
||||
export const createDeployment = async (
|
||||
@@ -57,6 +79,7 @@ export const createDeployment = async (
|
||||
try {
|
||||
await removeLastTenDeployments(
|
||||
deployment.applicationId,
|
||||
"application",
|
||||
application.serverId,
|
||||
);
|
||||
const { LOGS_PATH } = paths(!!application.serverId);
|
||||
@@ -88,21 +111,35 @@ export const createDeployment = async (
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
applicationId: deployment.applicationId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
await updateApplicationStatus(application.applicationId, "error");
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -117,8 +154,9 @@ export const createDeploymentPreview = async (
|
||||
deployment.previewDeploymentId,
|
||||
);
|
||||
try {
|
||||
await removeLastTenPreviewDeploymenById(
|
||||
await removeLastTenDeployments(
|
||||
deployment.previewDeploymentId,
|
||||
"previewDeployment",
|
||||
previewDeployment?.application?.serverId,
|
||||
);
|
||||
|
||||
@@ -154,23 +192,37 @@ export const createDeploymentPreview = async (
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
previewDeploymentId: deployment.previewDeploymentId,
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
previewDeploymentId: deployment.previewDeploymentId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
await updatePreviewDeployment(deployment.previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -183,8 +235,9 @@ export const createDeploymentCompose = async (
|
||||
) => {
|
||||
const compose = await findComposeById(deployment.composeId);
|
||||
try {
|
||||
await removeLastTenComposeDeployments(
|
||||
await removeLastTenDeployments(
|
||||
deployment.composeId,
|
||||
"compose",
|
||||
compose.serverId,
|
||||
);
|
||||
const { LOGS_PATH } = paths(!!compose.serverId);
|
||||
@@ -216,23 +269,278 @@ echo "Initializing deployment" >> ${logFilePath};
|
||||
description: deployment.description || "",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
composeId: deployment.composeId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
await updateCompose(compose.composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentBackup = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentBackup._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const backup = await findBackupById(deployment.backupId);
|
||||
|
||||
let serverId: string | null | undefined;
|
||||
if (backup.backupType === "database") {
|
||||
serverId =
|
||||
backup.postgres?.serverId ||
|
||||
backup.mariadb?.serverId ||
|
||||
backup.mysql?.serverId ||
|
||||
backup.mongo?.serverId;
|
||||
} else if (backup.backupType === "compose") {
|
||||
serverId = backup.compose?.serverId;
|
||||
}
|
||||
try {
|
||||
await removeLastTenDeployments(deployment.backupId, "backup", serverId);
|
||||
const { LOGS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${backup.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(LOGS_PATH, backup.appName, fileName);
|
||||
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${LOGS_PATH}/${backup.appName};
|
||||
echo "Initializing backup\n" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
} else {
|
||||
await fsPromises.mkdir(path.join(LOGS_PATH, backup.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing backup\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
backupId: deployment.backupId,
|
||||
title: deployment.title || "Backup",
|
||||
description: deployment.description || "",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the backup",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
backupId: deployment.backupId,
|
||||
title: deployment.title || "Backup",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the backup",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentSchedule = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentSchedule._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const schedule = await findScheduleById(deployment.scheduleId);
|
||||
|
||||
try {
|
||||
const serverId =
|
||||
schedule.application?.serverId ||
|
||||
schedule.compose?.serverId ||
|
||||
schedule.server?.serverId;
|
||||
await removeLastTenDeployments(deployment.scheduleId, "schedule", serverId);
|
||||
const { SCHEDULES_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${schedule.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(SCHEDULES_PATH, schedule.appName, fileName);
|
||||
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${SCHEDULES_PATH}/${schedule.appName};
|
||||
echo "Initializing schedule" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
} else {
|
||||
await fsPromises.mkdir(path.join(SCHEDULES_PATH, schedule.appName), {
|
||||
recursive: true,
|
||||
});
|
||||
await fsPromises.writeFile(logFilePath, "Initializing schedule\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
scheduleId: deployment.scheduleId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
scheduleId: deployment.scheduleId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentVolumeBackup = async (
|
||||
deployment: Omit<
|
||||
typeof apiCreateDeploymentVolumeBackup._type,
|
||||
"deploymentId" | "createdAt" | "status" | "logPath"
|
||||
>,
|
||||
) => {
|
||||
const volumeBackup = await findVolumeBackupById(deployment.volumeBackupId);
|
||||
|
||||
try {
|
||||
const serverId =
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
await removeLastTenDeployments(
|
||||
deployment.volumeBackupId,
|
||||
"volumeBackup",
|
||||
serverId,
|
||||
);
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss");
|
||||
const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`;
|
||||
const logFilePath = path.join(
|
||||
VOLUME_BACKUPS_PATH,
|
||||
volumeBackup.appName,
|
||||
fileName,
|
||||
);
|
||||
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const command = `
|
||||
mkdir -p ${VOLUME_BACKUPS_PATH}/${volumeBackup.appName};
|
||||
echo "Initializing volume backup" >> ${logFilePath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(server.serverId, command);
|
||||
} else {
|
||||
await fsPromises.mkdir(
|
||||
path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName),
|
||||
{
|
||||
recursive: true,
|
||||
},
|
||||
);
|
||||
await fsPromises.writeFile(logFilePath, "Initializing volume backup\n");
|
||||
}
|
||||
|
||||
const deploymentCreate = await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
volumeBackupId: deployment.volumeBackupId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "running",
|
||||
logPath: logFilePath,
|
||||
description: deployment.description || "",
|
||||
startedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
volumeBackupId: deployment.volumeBackupId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -245,9 +553,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 to delete this deployment",
|
||||
message,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -261,109 +571,25 @@ export const removeDeploymentsByApplicationId = async (
|
||||
.returning();
|
||||
};
|
||||
|
||||
const removeLastTenDeployments = async (
|
||||
applicationId: string,
|
||||
serverId: string | null,
|
||||
const getDeploymentsByType = async (
|
||||
id: string,
|
||||
type:
|
||||
| "application"
|
||||
| "compose"
|
||||
| "server"
|
||||
| "schedule"
|
||||
| "previewDeployment"
|
||||
| "backup"
|
||||
| "volumeBackup",
|
||||
) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
where: eq(deployments[`${type}Id`], id),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
with: {
|
||||
rollback: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (deploymentList.length > 10) {
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
if (serverId) {
|
||||
let command = "";
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (existsSync(logPath)) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeLastTenComposeDeployments = async (
|
||||
composeId: string,
|
||||
serverId: string | null,
|
||||
) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.composeId, composeId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
if (deploymentList.length > 10) {
|
||||
if (serverId) {
|
||||
let command = "";
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (existsSync(logPath)) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const removeLastTenPreviewDeploymenById = async (
|
||||
previewDeploymentId: string,
|
||||
serverId: string | null,
|
||||
) => {
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.previewDeploymentId, previewDeploymentId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
|
||||
if (deploymentList.length > 10) {
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
if (serverId) {
|
||||
let command = "";
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (existsSync(logPath)) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return deploymentList;
|
||||
};
|
||||
|
||||
export const removeDeployments = async (application: Application) => {
|
||||
@@ -378,6 +604,57 @@ export const removeDeployments = async (application: Application) => {
|
||||
await removeDeploymentsByApplicationId(applicationId);
|
||||
};
|
||||
|
||||
const removeLastTenDeployments = async (
|
||||
id: string,
|
||||
type:
|
||||
| "application"
|
||||
| "compose"
|
||||
| "server"
|
||||
| "schedule"
|
||||
| "previewDeployment"
|
||||
| "backup"
|
||||
| "volumeBackup",
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
const deploymentList = await getDeploymentsByType(id, type);
|
||||
if (deploymentList.length > 10) {
|
||||
const deploymentsToDelete = deploymentList.slice(10);
|
||||
if (serverId) {
|
||||
let command = "";
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
|
||||
if (logPath !== ".") {
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
`;
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (
|
||||
existsSync(logPath) &&
|
||||
!oldDeployment.errorMessage &&
|
||||
logPath !== "."
|
||||
) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeploymentsByPreviewDeploymentId = async (
|
||||
previewDeployment: PreviewDeployment,
|
||||
serverId: string | null,
|
||||
@@ -459,6 +736,10 @@ export const updateDeploymentStatus = async (
|
||||
.update(deployments)
|
||||
.set({
|
||||
status: deploymentStatus,
|
||||
finishedAt:
|
||||
deploymentStatus === "done" || deploymentStatus === "error"
|
||||
? new Date().toISOString()
|
||||
: null,
|
||||
})
|
||||
.where(eq(deployments.deploymentId, deploymentId))
|
||||
.returning();
|
||||
@@ -497,14 +778,16 @@ export const createServerDeployment = async (
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Error creating the deployment";
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -58,7 +58,11 @@ export const getContainers = async (serverId?: string | null) => {
|
||||
serverId,
|
||||
};
|
||||
})
|
||||
.filter((container) => !container.name.includes("dokploy"));
|
||||
.filter(
|
||||
(container) =>
|
||||
!container.name.includes("dokploy") ||
|
||||
container.name.includes("dokploy-monitoring"),
|
||||
);
|
||||
|
||||
return containers;
|
||||
} catch (error) {
|
||||
@@ -94,7 +98,7 @@ export const getConfig = async (
|
||||
const config = JSON.parse(stdout);
|
||||
|
||||
return config;
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getContainersByAppNameMatch = async (
|
||||
@@ -152,20 +156,144 @@ export const getContainersByAppNameMatch = async (
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getStackContainersByAppName = async (
|
||||
appName: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker stack ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
result = stdout.trim().split("\n");
|
||||
} else {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
|
||||
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 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,
|
||||
};
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getServiceContainersByAppName = async (
|
||||
appName: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker service ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
result = stdout.trim().split("\n");
|
||||
} else {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
|
||||
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 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,
|
||||
};
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch {}
|
||||
|
||||
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;
|
||||
@@ -203,7 +331,7 @@ export const getContainersByAppLabel = async (
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
};
|
||||
@@ -222,5 +350,125 @@ export const containerRestart = async (containerId: string) => {
|
||||
const config = JSON.parse(stdout);
|
||||
|
||||
return config;
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getSwarmNodes = async (serverId?: string) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const command = "docker node ls --format '{{json .}}'";
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodesArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
return nodesArray;
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getNodeInfo = async (nodeId: string, serverId?: string) => {
|
||||
try {
|
||||
const command = `docker node inspect ${nodeId} --format '{{json .}}'`;
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeInfo = JSON.parse(stdout);
|
||||
|
||||
return nodeInfo;
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getNodeApplications = async (serverId?: string) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const command = `docker service ls --format '{{json .}}'`;
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const appArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line))
|
||||
.filter((service) => !service.Name.startsWith("dokploy-"));
|
||||
|
||||
return appArray;
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getApplicationInfo = async (
|
||||
appNames: string[],
|
||||
serverId?: string,
|
||||
) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const command = `docker service ps ${appNames.join(" ")} --format '{{json .}}' --no-trunc`;
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const appArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return appArray;
|
||||
} catch {}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import dns from "node:dns";
|
||||
import { promisify } from "node:util";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { generateRandomDomain } from "@dokploy/server/templates/utils";
|
||||
import { generateRandomDomain } from "@dokploy/server/templates";
|
||||
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 { detectCDNProvider } from "./cdn";
|
||||
import { findServerById } from "./server";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
@@ -40,7 +43,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 +60,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 +129,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))
|
||||
@@ -138,3 +140,64 @@ export const removeDomainById = async (domainId: string) => {
|
||||
export const getDomainHost = (domain: Domain) => {
|
||||
return `${domain.https ? "https" : "http"}://${domain.host}`;
|
||||
};
|
||||
|
||||
const resolveDns = promisify(dns.resolve4);
|
||||
|
||||
export const validateDomain = async (
|
||||
domain: string,
|
||||
expectedIp?: string,
|
||||
): Promise<{
|
||||
isValid: boolean;
|
||||
resolvedIp?: string;
|
||||
error?: string;
|
||||
isCloudflare?: boolean;
|
||||
cdnProvider?: string;
|
||||
}> => {
|
||||
try {
|
||||
// Remove protocol and path if present
|
||||
const cleanDomain = domain.replace(/^https?:\/\//, "").split("/")[0];
|
||||
|
||||
// Resolve the domain to get its IP
|
||||
const ips = await resolveDns(cleanDomain || "");
|
||||
|
||||
const resolvedIps = ips.map((ip) => ip.toString());
|
||||
|
||||
// Check if any IP belongs to a CDN provider
|
||||
const cdnProvider = ips
|
||||
.map((ip) => detectCDNProvider(ip))
|
||||
.find((provider) => provider !== null);
|
||||
|
||||
// If behind a CDN, we consider it valid but inform the user
|
||||
if (cdnProvider) {
|
||||
return {
|
||||
isValid: true,
|
||||
resolvedIp: resolvedIps.join(", "),
|
||||
cdnProvider: cdnProvider.displayName,
|
||||
error: cdnProvider.warningMessage,
|
||||
};
|
||||
}
|
||||
|
||||
// If we have an expected IP, validate against it
|
||||
if (expectedIp) {
|
||||
return {
|
||||
isValid: resolvedIps.includes(expectedIp),
|
||||
resolvedIp: resolvedIps.join(", "),
|
||||
error: !resolvedIps.includes(expectedIp)
|
||||
? `Domain resolves to ${resolvedIps.join(", ")} but should point to ${expectedIp}`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// If no expected IP, just return the resolved IP
|
||||
return {
|
||||
isValid: true,
|
||||
resolvedIp: resolvedIps.join(", "),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : "Failed to resolve domain",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateGitea,
|
||||
gitProvider,
|
||||
gitea,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export type Gitea = typeof gitea.$inferSelect;
|
||||
|
||||
export const createGitea = async (
|
||||
input: typeof apiCreateGitea._type,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "gitea",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the Git provider",
|
||||
});
|
||||
}
|
||||
|
||||
const giteaProvider = await tx
|
||||
.insert(gitea)
|
||||
.values({
|
||||
...input,
|
||||
gitProviderId: newGitProvider?.gitProviderId,
|
||||
})
|
||||
.returning()
|
||||
.then((response: (typeof gitea.$inferSelect)[]) => response[0]);
|
||||
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the Gitea provider",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
giteaId: giteaProvider.giteaId,
|
||||
clientId: giteaProvider.clientId,
|
||||
giteaUrl: giteaProvider.giteaUrl,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const findGiteaById = async (giteaId: string) => {
|
||||
try {
|
||||
const giteaProviderResult = await db.query.gitea.findFirst({
|
||||
where: eq(gitea.giteaId, giteaId),
|
||||
with: {
|
||||
gitProvider: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!giteaProviderResult) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
return giteaProviderResult;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateGitea = async (giteaId: string, input: Partial<Gitea>) => {
|
||||
try {
|
||||
const updateResult = await db
|
||||
.update(gitea)
|
||||
.set(input)
|
||||
.where(eq(gitea.giteaId, giteaId))
|
||||
.returning();
|
||||
|
||||
const result = updateResult[0] as Gitea | undefined;
|
||||
|
||||
if (!result) {
|
||||
throw new Error(`Failed to update Gitea provider with ID ${giteaId}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -12,15 +12,17 @@ import { updatePreviewDeployment } from "./preview-deployment";
|
||||
export type Github = typeof github.$inferSelect;
|
||||
export const createGithub = async (
|
||||
input: typeof apiCreateGithub._type,
|
||||
adminId: string,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "github",
|
||||
adminId: adminId,
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
@@ -28,7 +30,7 @@ export const createGithub = async (
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the git provider",
|
||||
message: "Error creating the Git provider",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ export const issueCommentExists = async ({
|
||||
comment_id: comment_id,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -190,3 +192,156 @@ export const createPreviewDeploymentComment = async ({
|
||||
pullRequestCommentId: `${issue.data.id}`,
|
||||
}).then((response) => response[0]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate security notification message for blocked PR deployments
|
||||
*/
|
||||
export const getSecurityBlockedMessage = (
|
||||
prAuthor: string,
|
||||
repositoryName: string,
|
||||
permission: string | null,
|
||||
) => {
|
||||
return `### 🚨 Preview Deployment Blocked - Security Protection
|
||||
|
||||
**Your pull request was blocked from triggering preview deployments**
|
||||
|
||||
#### Why was this blocked?
|
||||
- **User**: \`${prAuthor}\`
|
||||
- **Repository**: \`${repositoryName}\`
|
||||
- **Permission Level**: \`${permission || "none"}\`
|
||||
- **Required Level**: \`write\`, \`maintain\`, or \`admin\`
|
||||
|
||||
#### How to resolve this:
|
||||
|
||||
**Option 1: Get Collaborator Access (Recommended)**
|
||||
Ask a repository maintainer to invite you as a collaborator with **write permissions** or higher.
|
||||
|
||||
**Option 2: Request Permission Override**
|
||||
Ask a repository administrator to disable security validation for this specific application if appropriate.
|
||||
|
||||
#### For Repository Administrators:
|
||||
To disable this security check (⚠️ **not recommended for public repositories**):
|
||||
Enter to preview settings and disable the security check.
|
||||
|
||||
---
|
||||
*This security measure protects against malicious code execution in preview deployments. Only trusted collaborators should have the ability to trigger deployments.*
|
||||
|
||||
<details>
|
||||
<summary>🛡️ Learn more about this security feature</summary>
|
||||
|
||||
This protection prevents unauthorized users from:
|
||||
- Executing malicious code on the deployment server
|
||||
- Accessing environment variables and secrets
|
||||
- Potentially compromising the infrastructure
|
||||
|
||||
Preview deployments are powerful but require trust. Only users with repository write access can trigger them.
|
||||
</details>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a security notification comment already exists on a GitHub PR
|
||||
* This prevents creating duplicate security comments on subsequent pushes
|
||||
*/
|
||||
export const hasExistingSecurityComment = async ({
|
||||
owner,
|
||||
repository,
|
||||
prNumber,
|
||||
githubId,
|
||||
}: {
|
||||
owner: string;
|
||||
repository: string;
|
||||
prNumber: number;
|
||||
githubId: string;
|
||||
}): Promise<boolean> => {
|
||||
try {
|
||||
const github = await findGithubById(githubId);
|
||||
const octokit = authGithub(github);
|
||||
|
||||
// Get all comments for this PR
|
||||
const { data: comments } = await octokit.rest.issues.listComments({
|
||||
owner,
|
||||
repo: repository,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
|
||||
// Check if any comment contains our security notification marker
|
||||
const securityCommentExists = comments.some((comment) =>
|
||||
comment.body?.includes(
|
||||
"🚨 Preview Deployment Blocked - Security Protection",
|
||||
),
|
||||
);
|
||||
|
||||
return securityCommentExists;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ Failed to check existing comments on PR #${prNumber}:`,
|
||||
error,
|
||||
);
|
||||
// If we can't check, assume no comment exists to avoid blocking functionality
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a security notification comment on a GitHub PR
|
||||
*/
|
||||
export const createSecurityBlockedComment = async ({
|
||||
owner,
|
||||
repository,
|
||||
prNumber,
|
||||
prAuthor,
|
||||
permission,
|
||||
githubId,
|
||||
}: {
|
||||
owner: string;
|
||||
repository: string;
|
||||
prNumber: number;
|
||||
prAuthor: string;
|
||||
permission: string | null;
|
||||
githubId: string;
|
||||
}) => {
|
||||
try {
|
||||
// Check if a security comment already exists to prevent duplicates
|
||||
const commentExists = await hasExistingSecurityComment({
|
||||
owner,
|
||||
repository,
|
||||
prNumber,
|
||||
githubId,
|
||||
});
|
||||
|
||||
if (commentExists) {
|
||||
console.log(
|
||||
`ℹ️ Security notification comment already exists on PR #${prNumber}, skipping duplicate`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const github = await findGithubById(githubId);
|
||||
const octokit = authGithub(github);
|
||||
|
||||
const securityMessage = getSecurityBlockedMessage(
|
||||
prAuthor,
|
||||
repository,
|
||||
permission,
|
||||
);
|
||||
|
||||
const issue = await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo: repository,
|
||||
issue_number: prNumber,
|
||||
body: securityMessage,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`✅ Security notification comment created on PR #${prNumber}: ${issue.data.html_url}`,
|
||||
);
|
||||
return issue.data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ Failed to create security comment on PR #${prNumber}:`,
|
||||
error,
|
||||
);
|
||||
// Don't throw error - security comment is nice-to-have, not critical
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,15 +11,17 @@ export type Gitlab = typeof gitlab.$inferSelect;
|
||||
|
||||
export const createGitlab = async (
|
||||
input: typeof apiCreateGitlab._type,
|
||||
adminId: string,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "gitlab",
|
||||
adminId: adminId,
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
@@ -29,7 +29,7 @@ export const createGitlab = async (
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the git provider",
|
||||
message: "Error creating the Git provider",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
backups,
|
||||
mariadb,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
export type Mariadb = typeof mariadb.$inferSelect;
|
||||
|
||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("mariadb", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newMariadb = await db
|
||||
@@ -40,6 +37,7 @@ export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||
databaseRootPassword: input.databaseRootPassword
|
||||
? input.databaseRootPassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -65,6 +63,7 @@ export const findMariadbById = async (mariadbId: string) => {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -82,10 +81,11 @@ export const updateMariadbById = async (
|
||||
mariadbId: string,
|
||||
mariadbData: Partial<Mariadb>,
|
||||
) => {
|
||||
const { appName, ...rest } = mariadbData;
|
||||
const result = await db
|
||||
.update(mariadb)
|
||||
.set({
|
||||
...mariadbData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(mariadb.mariadbId, mariadbId))
|
||||
.returning();
|
||||
@@ -121,23 +121,33 @@ export const findMariadbByBackupId = async (backupId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployMariadb = async (mariadbId: string) => {
|
||||
export const deployMariadb = async (
|
||||
mariadbId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const mariadb = await findMariadbById(mariadbId);
|
||||
try {
|
||||
await updateMariadbById(mariadbId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
onData?.("Starting mariadb deployment...");
|
||||
if (mariadb.serverId) {
|
||||
await execAsyncRemote(
|
||||
mariadb.serverId,
|
||||
`docker pull ${mariadb.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(mariadb.dockerImage);
|
||||
await pullImage(mariadb.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildMariadb(mariadb);
|
||||
await updateMariadbById(mariadbId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateMariadbById(mariadbId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import {
|
||||
type apiCreateMongo,
|
||||
backups,
|
||||
compose,
|
||||
mongo,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -13,17 +18,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
export type Mongo = typeof mongo.$inferSelect;
|
||||
|
||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mongo");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("mongo", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newMongo = await db
|
||||
@@ -33,6 +35,7 @@ export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -57,6 +60,7 @@ export const findMongoById = async (mongoId: string) => {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -74,10 +78,11 @@ export const updateMongoById = async (
|
||||
mongoId: string,
|
||||
mongoData: Partial<Mongo>,
|
||||
) => {
|
||||
const { appName, ...rest } = mongoData;
|
||||
const result = await db
|
||||
.update(mongo)
|
||||
.set({
|
||||
...mongoData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(mongo.mongoId, mongoId))
|
||||
.returning();
|
||||
@@ -104,6 +109,25 @@ export const findMongoByBackupId = async (backupId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findComposeByBackupId = async (backupId: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
...getTableColumns(compose),
|
||||
})
|
||||
.from(compose)
|
||||
.innerJoin(backups, eq(compose.composeId, backups.composeId))
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Compose not found",
|
||||
});
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeMongoById = async (mongoId: string) => {
|
||||
const result = await db
|
||||
.delete(mongo)
|
||||
@@ -113,20 +137,34 @@ export const removeMongoById = async (mongoId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployMongo = async (mongoId: string) => {
|
||||
export const deployMongo = async (
|
||||
mongoId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const mongo = await findMongoById(mongoId);
|
||||
try {
|
||||
await updateMongoById(mongoId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
|
||||
onData?.("Starting mongo deployment...");
|
||||
if (mongo.serverId) {
|
||||
await execAsyncRemote(mongo.serverId, `docker pull ${mongo.dockerImage}`);
|
||||
await execAsyncRemote(
|
||||
mongo.serverId,
|
||||
`docker pull ${mongo.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(mongo.dockerImage);
|
||||
await pullImage(mongo.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildMongo(mongo);
|
||||
await updateMongoById(mongoId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateMongoById(mongoId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
if (!value) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mount",
|
||||
message: "Error inserting mount",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the mount",
|
||||
message: `Error ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -88,10 +88,10 @@ export const createFileMount = async (mountId: string) => {
|
||||
await createFile(baseFilePath, mount.filePath || "", mount.content || "");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error to create the file mount: ${error}`);
|
||||
console.log(`Error creating the file mount: ${error}`);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the mount",
|
||||
message: `Error creating the mount ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
@@ -144,7 +144,8 @@ export const updateMount = async (
|
||||
await deleteFileMount(mountId);
|
||||
await createFileMount(mountId);
|
||||
}
|
||||
return mount;
|
||||
|
||||
return await findMountById(mountId);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -211,7 +212,7 @@ export const deleteFileMount = async (mountId: string) => {
|
||||
} else {
|
||||
await removeFileOrDirectory(fullPath);
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getBaseFilesPath = async (mountId: string) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -13,18 +13,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
export type MySql = typeof mysql.$inferSelect;
|
||||
|
||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
|
||||
const appName = buildAppName("mysql", input.appName);
|
||||
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newMysql = await db
|
||||
@@ -37,6 +33,7 @@ export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||
databaseRootPassword: input.databaseRootPassword
|
||||
? input.databaseRootPassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -62,6 +59,7 @@ export const findMySqlById = async (mysqlId: string) => {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -79,10 +77,11 @@ export const updateMySqlById = async (
|
||||
mysqlId: string,
|
||||
mysqlData: Partial<MySql>,
|
||||
) => {
|
||||
const { appName, ...rest } = mysqlData;
|
||||
const result = await db
|
||||
.update(mysql)
|
||||
.set({
|
||||
...mysqlData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(mysql.mysqlId, mysqlId))
|
||||
.returning();
|
||||
@@ -118,20 +117,33 @@ export const removeMySqlById = async (mysqlId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployMySql = async (mysqlId: string) => {
|
||||
export const deployMySql = async (
|
||||
mysqlId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const mysql = await findMySqlById(mysqlId);
|
||||
try {
|
||||
await updateMySqlById(mysqlId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
onData?.("Starting mysql deployment...");
|
||||
if (mysql.serverId) {
|
||||
await execAsyncRemote(mysql.serverId, `docker pull ${mysql.dockerImage}`);
|
||||
await execAsyncRemote(
|
||||
mysql.serverId,
|
||||
`docker pull ${mysql.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(mysql.dockerImage);
|
||||
await pullImage(mysql.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildMysql(mysql);
|
||||
await updateMySqlById(mysqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateMySqlById(mysqlId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -2,14 +2,17 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateDiscord,
|
||||
type apiCreateEmail,
|
||||
type apiCreateGotify,
|
||||
type apiCreateSlack,
|
||||
type apiCreateTelegram,
|
||||
type apiUpdateDiscord,
|
||||
type apiUpdateEmail,
|
||||
type apiUpdateGotify,
|
||||
type apiUpdateSlack,
|
||||
type apiUpdateTelegram,
|
||||
discord,
|
||||
email,
|
||||
gotify,
|
||||
notifications,
|
||||
slack,
|
||||
telegram,
|
||||
@@ -21,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
|
||||
@@ -51,7 +54,8 @@ export const createSlackNotification = async (
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "slack",
|
||||
adminId: adminId,
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -80,7 +84,8 @@ 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))
|
||||
.returning()
|
||||
@@ -109,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
|
||||
@@ -117,6 +122,7 @@ export const createTelegramNotification = async (
|
||||
.values({
|
||||
botToken: input.botToken,
|
||||
chatId: input.chatId,
|
||||
messageThreadId: input.messageThreadId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -139,7 +145,8 @@ export const createTelegramNotification = async (
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "telegram",
|
||||
adminId: adminId,
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -168,7 +175,8 @@ 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))
|
||||
.returning()
|
||||
@@ -186,6 +194,7 @@ export const updateTelegramNotification = async (
|
||||
.set({
|
||||
botToken: input.botToken,
|
||||
chatId: input.chatId,
|
||||
messageThreadId: input.messageThreadId,
|
||||
})
|
||||
.where(eq(telegram.telegramId, input.telegramId))
|
||||
.returning()
|
||||
@@ -197,13 +206,14 @@ 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
|
||||
.insert(discord)
|
||||
.values({
|
||||
webhookUrl: input.webhookUrl,
|
||||
decoration: input.decoration,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -226,7 +236,8 @@ export const createDiscordNotification = async (
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "discord",
|
||||
adminId: adminId,
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -255,7 +266,8 @@ 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))
|
||||
.returning()
|
||||
@@ -272,6 +284,7 @@ export const updateDiscordNotification = async (
|
||||
.update(discord)
|
||||
.set({
|
||||
webhookUrl: input.webhookUrl,
|
||||
decoration: input.decoration,
|
||||
})
|
||||
.where(eq(discord.discordId, input.discordId))
|
||||
.returning()
|
||||
@@ -283,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
|
||||
@@ -317,7 +330,8 @@ export const createEmailNotification = async (
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "email",
|
||||
adminId: adminId,
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -346,7 +360,8 @@ 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))
|
||||
.returning()
|
||||
@@ -377,6 +392,96 @@ export const updateEmailNotification = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const createGotifyNotification = async (
|
||||
input: typeof apiCreateGotify._type,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newGotify = await tx
|
||||
.insert(gotify)
|
||||
.values({
|
||||
serverUrl: input.serverUrl,
|
||||
appToken: input.appToken,
|
||||
priority: input.priority,
|
||||
decoration: input.decoration,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newGotify) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting gotify",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
gotifyId: newGotify.gotifyId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "gotify",
|
||||
organizationId: organizationId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateGotifyNotification = async (
|
||||
input: typeof apiUpdateGotify._type,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error Updating notification",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(gotify)
|
||||
.set({
|
||||
serverUrl: input.serverUrl,
|
||||
appToken: input.appToken,
|
||||
priority: input.priority,
|
||||
decoration: input.decoration,
|
||||
})
|
||||
.where(eq(gotify.gotifyId, input.gotifyId));
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const findNotificationById = async (notificationId: string) => {
|
||||
const notification = await db.query.notifications.findFirst({
|
||||
where: eq(notifications.notificationId, notificationId),
|
||||
@@ -385,6 +490,7 @@ export const findNotificationById = async (notificationId: string) => {
|
||||
telegram: true,
|
||||
discord: true,
|
||||
email: true,
|
||||
gotify: true,
|
||||
},
|
||||
});
|
||||
if (!notification) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
backups,
|
||||
postgres,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
export type Postgres = typeof postgres.$inferSelect;
|
||||
|
||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("postgres", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newPostgres = await db
|
||||
@@ -37,6 +34,7 @@ export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -60,6 +58,7 @@ export const findPostgresById = async (postgresId: string) => {
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -96,10 +95,11 @@ export const updatePostgresById = async (
|
||||
postgresId: string,
|
||||
postgresData: Partial<Postgres>,
|
||||
) => {
|
||||
const { appName, ...rest } = postgresData;
|
||||
const result = await db
|
||||
.update(postgres)
|
||||
.set({
|
||||
...postgresData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(postgres.postgresId, postgresId))
|
||||
.returning();
|
||||
@@ -116,24 +116,37 @@ export const removePostgresById = async (postgresId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployPostgres = async (postgresId: string) => {
|
||||
export const deployPostgres = async (
|
||||
postgresId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const postgres = await findPostgresById(postgresId);
|
||||
try {
|
||||
const promises = [];
|
||||
await updatePostgresById(postgresId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
|
||||
onData?.("Starting postgres deployment...");
|
||||
|
||||
if (postgres.serverId) {
|
||||
const result = await execAsyncRemote(
|
||||
await execAsyncRemote(
|
||||
postgres.serverId,
|
||||
`docker pull ${postgres.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(postgres.dockerImage);
|
||||
await pullImage(postgres.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildPostgres(postgres);
|
||||
|
||||
await updatePostgresById(postgresId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updatePostgresById(postgresId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -2,25 +2,22 @@ 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 { findApplicationById } from "./application";
|
||||
import { createDomain } from "./domain";
|
||||
import { generatePassword, generateRandomDomain } from "../templates/utils";
|
||||
import { manageDomain } from "../utils/traefik/domain";
|
||||
import {
|
||||
removeDeployments,
|
||||
removeDeploymentsByPreviewDeploymentId,
|
||||
} from "./deployment";
|
||||
import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||
import { generatePassword } from "../templates";
|
||||
import { removeService } from "../utils/docker/utils";
|
||||
import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { getIssueComment, type Github } from "./github";
|
||||
import { findAdminById } from "./admin";
|
||||
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||
import { manageDomain } from "../utils/traefik/domain";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
||||
import { createDomain } from "./domain";
|
||||
import { type Github, getIssueComment } from "./github";
|
||||
|
||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||
|
||||
@@ -65,6 +62,7 @@ export const findApplicationByPreview = async (applicationId: string) => {
|
||||
gitlab: true,
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
@@ -106,13 +104,17 @@ export const removePreviewDeployment = async (previewDeploymentId: string) => {
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
}
|
||||
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 to delete this preview deployment",
|
||||
message,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -154,11 +156,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);
|
||||
@@ -166,7 +171,7 @@ export const createPreviewDeployment = async (
|
||||
const runningComment = getIssueComment(
|
||||
application.name,
|
||||
"initializing",
|
||||
generateDomain,
|
||||
`${application.previewHttps ? "https" : "http"}://${generateDomain}`,
|
||||
);
|
||||
|
||||
const issue = await octokit.rest.issues.createComment({
|
||||
@@ -189,7 +194,7 @@ export const createPreviewDeployment = async (
|
||||
if (!previewDeployment) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the preview deployment",
|
||||
message: "Error creating the preview deployment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,6 +204,7 @@ export const createPreviewDeployment = async (
|
||||
port: application.previewPort,
|
||||
https: application.previewHttps,
|
||||
certificateType: application.previewCertificateType,
|
||||
customCertResolver: application.previewCustomCertResolver,
|
||||
domainType: "preview",
|
||||
previewDeploymentId: previewDeployment.previewDeploymentId,
|
||||
});
|
||||
@@ -250,7 +256,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 +274,7 @@ const generateWildcardDomain = async (
|
||||
}
|
||||
|
||||
if (!ip) {
|
||||
const admin = await findAdminById(adminId);
|
||||
const admin = await findUserById(userId);
|
||||
ip = admin?.serverIp || "";
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
@@ -30,7 +30,7 @@ export const createProject = async (
|
||||
if (!newProject) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the project",
|
||||
message: "Error creating the project",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -40,7 +40,7 @@ export const createRedirect = async (
|
||||
if (!redirect) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the redirect",
|
||||
message: "Error creating the redirect",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const createRedirect = async (
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create this redirect",
|
||||
message: "Error creating this redirect",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export const removeRedirectById = async (redirectId: string) => {
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to remove this redirect",
|
||||
message: "Error removing this redirect",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -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 to update this redirect",
|
||||
message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateRedis, redis } from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildRedis } from "@dokploy/server/utils/databases/redis";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -14,17 +14,14 @@ export type Redis = typeof redis.$inferSelect;
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("redis");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("redis", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newRedis = await db
|
||||
@@ -34,6 +31,7 @@ export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -70,10 +68,11 @@ export const updateRedisById = async (
|
||||
redisId: string,
|
||||
redisData: Partial<Redis>,
|
||||
) => {
|
||||
const { appName, ...rest } = redisData;
|
||||
const result = await db
|
||||
.update(redis)
|
||||
.set({
|
||||
...redisData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(redis.redisId, redisId))
|
||||
.returning();
|
||||
@@ -90,20 +89,34 @@ export const removeRedisById = async (redisId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployRedis = async (redisId: string) => {
|
||||
export const deployRedis = async (
|
||||
redisId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const redis = await findRedisById(redisId);
|
||||
try {
|
||||
await updateRedisById(redisId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
|
||||
onData?.("Starting redis deployment...");
|
||||
if (redis.serverId) {
|
||||
await execAsyncRemote(redis.serverId, `docker pull ${redis.dockerImage}`);
|
||||
await execAsyncRemote(
|
||||
redis.serverId,
|
||||
`docker pull ${redis.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(redis.dockerImage);
|
||||
await pullImage(redis.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildRedis(redis);
|
||||
await updateRedisById(redisId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateRedisById(redisId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
@@ -71,7 +71,7 @@ export const removeRegistry = async (registryId: string) => {
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to remove this registry",
|
||||
message: "Error removing this registry",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -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 to update 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;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { db } from "../db";
|
||||
import {
|
||||
type createRollbackSchema,
|
||||
deployments as deploymentsSchema,
|
||||
rollbacks,
|
||||
} from "../db/schema";
|
||||
import { type ApplicationNested, getAuthConfig } from "../utils/builders";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateConfigContainer,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../utils/docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
import { type Application, findApplicationById } from "./application";
|
||||
import { findDeploymentById } from "./deployment";
|
||||
import type { Mount } from "./mount";
|
||||
import type { Port } from "./port";
|
||||
import type { Project } from "./project";
|
||||
|
||||
export const createRollback = async (
|
||||
input: z.infer<typeof createRollbackSchema>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const { fullContext, ...other } = input;
|
||||
const rollback = await tx
|
||||
.insert(rollbacks)
|
||||
.values(other)
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!rollback) {
|
||||
throw new Error("Failed to create rollback");
|
||||
}
|
||||
|
||||
const tagImage = `${input.appName}:v${rollback.version}`;
|
||||
const deployment = await findDeploymentById(rollback.deploymentId);
|
||||
|
||||
if (!deployment?.applicationId) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
const {
|
||||
deployments: _,
|
||||
bitbucket,
|
||||
github,
|
||||
gitlab,
|
||||
gitea,
|
||||
...rest
|
||||
} = await findApplicationById(deployment.applicationId);
|
||||
|
||||
await tx
|
||||
.update(rollbacks)
|
||||
.set({
|
||||
image: tagImage,
|
||||
fullContext: rest,
|
||||
})
|
||||
.where(eq(rollbacks.rollbackId, rollback.rollbackId));
|
||||
|
||||
// Update the deployment to reference this rollback
|
||||
await tx
|
||||
.update(deploymentsSchema)
|
||||
.set({
|
||||
rollbackId: rollback.rollbackId,
|
||||
})
|
||||
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
|
||||
|
||||
await createRollbackImage(rest, tagImage);
|
||||
|
||||
return rollback;
|
||||
});
|
||||
};
|
||||
|
||||
const findRollbackById = async (rollbackId: string) => {
|
||||
const result = await db.query.rollbacks.findFirst({
|
||||
where: eq(rollbacks.rollbackId, rollbackId),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Rollback not found");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const createRollbackImage = async (
|
||||
application: ApplicationNested,
|
||||
tagImage: string,
|
||||
) => {
|
||||
const docker = await getRemoteDocker(application.serverId);
|
||||
|
||||
const appTagName =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: `${application.appName}:latest`;
|
||||
|
||||
const result = docker.getImage(appTagName || "");
|
||||
|
||||
const [repo, version] = tagImage.split(":");
|
||||
|
||||
await result.tag({
|
||||
repo,
|
||||
tag: version,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
|
||||
const command = `docker image rm ${image} --force`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(command, serverId);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeRollbackById = async (rollbackId: string) => {
|
||||
const rollback = await findRollbackById(rollbackId);
|
||||
|
||||
if (!rollback) {
|
||||
throw new Error("Rollback not found");
|
||||
}
|
||||
|
||||
if (rollback?.image) {
|
||||
try {
|
||||
const deployment = await findDeploymentById(rollback.deploymentId);
|
||||
|
||||
if (!deployment?.applicationId) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
await deleteRollbackImage(rollback.image, application.serverId);
|
||||
|
||||
await db
|
||||
.delete(rollbacks)
|
||||
.where(eq(rollbacks.rollbackId, rollbackId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return rollback;
|
||||
};
|
||||
|
||||
export const rollback = async (rollbackId: string) => {
|
||||
const result = await findRollbackById(rollbackId);
|
||||
|
||||
const deployment = await findDeploymentById(result.deploymentId);
|
||||
|
||||
if (!deployment?.applicationId) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
|
||||
if (!result.fullContext) {
|
||||
throw new Error("Rollback context not found");
|
||||
}
|
||||
|
||||
// Use the full context for rollback
|
||||
await rollbackApplication(
|
||||
application.appName,
|
||||
result.image || "",
|
||||
application.serverId,
|
||||
result.fullContext,
|
||||
);
|
||||
};
|
||||
|
||||
const rollbackApplication = async (
|
||||
appName: string,
|
||||
image: string,
|
||||
serverId?: string | null,
|
||||
fullContext?: Application & {
|
||||
project: Project;
|
||||
mounts: Mount[];
|
||||
ports: Port[];
|
||||
},
|
||||
) => {
|
||||
if (!fullContext) {
|
||||
throw new Error("Full context is required for rollback");
|
||||
}
|
||||
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
|
||||
// Use the same configuration as mechanizeDockerContainer
|
||||
const {
|
||||
env,
|
||||
mounts,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuReservation,
|
||||
command,
|
||||
ports,
|
||||
} = fullContext;
|
||||
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
|
||||
const {
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
} = generateConfigContainer(fullContext as ApplicationNested);
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
fullContext.project.env,
|
||||
);
|
||||
|
||||
// For rollback, we use the provided image instead of calculating it
|
||||
const authConfig = getAuthConfig(fullContext as ApplicationNested);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
authconfig: authConfig,
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
PublishMode: port.publishMode,
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
})),
|
||||
},
|
||||
UpdateConfig,
|
||||
};
|
||||
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import path from "node:path";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { paths } from "../constants";
|
||||
import { db } from "../db";
|
||||
import { type Schedule, schedules } from "../db/schema/schedule";
|
||||
import type {
|
||||
createScheduleSchema,
|
||||
updateScheduleSchema,
|
||||
} from "../db/schema/schedule";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
|
||||
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;
|
||||
|
||||
export const createSchedule = async (
|
||||
input: z.infer<typeof createScheduleSchema>,
|
||||
) => {
|
||||
const { scheduleId, ...rest } = input;
|
||||
const [newSchedule] = await db.insert(schedules).values(rest).returning();
|
||||
|
||||
if (
|
||||
newSchedule &&
|
||||
(newSchedule.scheduleType === "dokploy-server" ||
|
||||
newSchedule.scheduleType === "server")
|
||||
) {
|
||||
await handleScript(newSchedule);
|
||||
}
|
||||
|
||||
return newSchedule;
|
||||
};
|
||||
|
||||
export const findScheduleById = async (scheduleId: string) => {
|
||||
const schedule = await db.query.schedules.findFirst({
|
||||
where: eq(schedules.scheduleId, scheduleId),
|
||||
with: {
|
||||
application: true,
|
||||
compose: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Schedule not found",
|
||||
});
|
||||
}
|
||||
return schedule;
|
||||
};
|
||||
|
||||
export const deleteSchedule = async (scheduleId: string) => {
|
||||
const schedule = await findScheduleById(scheduleId);
|
||||
const serverId =
|
||||
schedule?.serverId ||
|
||||
schedule?.application?.serverId ||
|
||||
schedule?.compose?.serverId;
|
||||
const { SCHEDULES_PATH } = paths(!!serverId);
|
||||
|
||||
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
|
||||
const command = `rm -rf ${fullPath}`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
const scheduleResult = await db
|
||||
.delete(schedules)
|
||||
.where(eq(schedules.scheduleId, scheduleId));
|
||||
if (!scheduleResult) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Schedule not found",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const updateSchedule = async (
|
||||
input: z.infer<typeof updateScheduleSchema>,
|
||||
) => {
|
||||
const { scheduleId, ...rest } = input;
|
||||
const [updatedSchedule] = await db
|
||||
.update(schedules)
|
||||
.set(rest)
|
||||
.where(eq(schedules.scheduleId, scheduleId))
|
||||
.returning();
|
||||
|
||||
if (!updatedSchedule) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Schedule not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
updatedSchedule?.scheduleType === "dokploy-server" ||
|
||||
updatedSchedule?.scheduleType === "server"
|
||||
) {
|
||||
await handleScript(updatedSchedule);
|
||||
}
|
||||
|
||||
return updatedSchedule;
|
||||
};
|
||||
|
||||
const handleScript = async (schedule: Schedule) => {
|
||||
const { SCHEDULES_PATH } = paths(!!schedule?.serverId);
|
||||
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
|
||||
|
||||
// Add PID and Schedule ID echo by default to all scripts
|
||||
const scriptWithPid = `echo "PID: $$ | Schedule ID: ${schedule.scheduleId}"
|
||||
${schedule?.script || ""}`;
|
||||
|
||||
const encodedContent = encodeBase64(scriptWithPid);
|
||||
const script = `
|
||||
mkdir -p ${fullPath}
|
||||
rm -f ${fullPath}/script.sh
|
||||
touch ${fullPath}/script.sh
|
||||
chmod +x ${fullPath}/script.sh
|
||||
echo "${encodedContent}" | base64 -d > ${fullPath}/script.sh
|
||||
`;
|
||||
|
||||
if (schedule?.scheduleType === "dokploy-server") {
|
||||
await execAsync(script);
|
||||
} else if (schedule?.scheduleType === "server") {
|
||||
await execAsyncRemote(schedule?.serverId || "", script);
|
||||
}
|
||||
};
|
||||
@@ -41,7 +41,7 @@ export const createSecurity = async (
|
||||
if (!securityResponse) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the security",
|
||||
message: "Error creating the security",
|
||||
});
|
||||
}
|
||||
await createSecurityMiddleware(application, securityResponse);
|
||||
@@ -50,7 +50,7 @@ export const createSecurity = async (
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create this security",
|
||||
message: "Error creating this security",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -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 to remove 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 to update this security",
|
||||
message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
@@ -21,7 +26,7 @@ export const createServer = async (
|
||||
if (!newServer) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the server",
|
||||
message: "Error creating the server",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,41 +1,107 @@
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { docker } from "@dokploy/server/constants";
|
||||
import { getServiceContainer } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
// import packageInfo from "../../../package.json";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
|
||||
const updateIsAvailable = async () => {
|
||||
try {
|
||||
const service = await getServiceContainer("dokploy");
|
||||
export interface IUpdateData {
|
||||
latestVersion: string | null;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
|
||||
const localImage = await docker.getImage(getDokployImage()).inspect();
|
||||
return localImage.Id !== service?.ImageID;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
export const DEFAULT_UPDATE_DATA: IUpdateData = {
|
||||
latestVersion: null,
|
||||
updateAvailable: false,
|
||||
};
|
||||
|
||||
/** Returns current Dokploy docker image tag or `latest` by default. */
|
||||
export const getDokployImageTag = () => {
|
||||
return process.env.RELEASE_TAG || "latest";
|
||||
};
|
||||
|
||||
export const getDokployImage = () => {
|
||||
return `dokploy/dokploy:${process.env.RELEASE_TAG || "latest"}`;
|
||||
return `dokploy/dokploy:${getDokployImageTag()}`;
|
||||
};
|
||||
|
||||
export const pullLatestRelease = async () => {
|
||||
try {
|
||||
const stream = await docker.pull(getDokployImage(), {});
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.modem.followProgress(stream, (err, res) =>
|
||||
err ? reject(err) : resolve(res),
|
||||
);
|
||||
});
|
||||
const newUpdateIsAvailable = await updateIsAvailable();
|
||||
return newUpdateIsAvailable;
|
||||
} catch (error) {}
|
||||
|
||||
return false;
|
||||
const stream = await docker.pull(getDokployImage());
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.modem.followProgress(stream, (err, res) =>
|
||||
err ? reject(err) : resolve(res),
|
||||
);
|
||||
});
|
||||
};
|
||||
export const getDokployVersion = () => {
|
||||
// return packageInfo.version;
|
||||
|
||||
/** Returns Dokploy docker service image digest */
|
||||
export const getServiceImageDigest = async () => {
|
||||
const { stdout } = await execAsync(
|
||||
"docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'",
|
||||
);
|
||||
|
||||
const currentDigest = stdout.trim().split("@")[1];
|
||||
|
||||
if (!currentDigest) {
|
||||
throw new Error("Could not get current service image digest");
|
||||
}
|
||||
|
||||
return currentDigest;
|
||||
};
|
||||
|
||||
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
|
||||
export const getUpdateData = async (): Promise<IUpdateData> => {
|
||||
let currentDigest: string;
|
||||
try {
|
||||
currentDigest = await getServiceImageDigest();
|
||||
} catch {
|
||||
// Docker service might not exist locally
|
||||
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
|
||||
// https://docs.dokploy.com/docs/core/manual-installation
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
|
||||
let url: string | null = `${baseUrl}?page_size=100`;
|
||||
let allResults: { digest: string; name: string }[] = [];
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const data = (await response.json()) as {
|
||||
next: string | null;
|
||||
results: { digest: string; name: string }[];
|
||||
};
|
||||
|
||||
allResults = allResults.concat(data.results);
|
||||
url = data?.next;
|
||||
}
|
||||
|
||||
const imageTag = getDokployImageTag();
|
||||
const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
|
||||
|
||||
if (!searchedDigest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
if (imageTag === "latest") {
|
||||
const versionedTag = allResults.find(
|
||||
(t) => t.digest === searchedDigest && t.name.startsWith("v"),
|
||||
);
|
||||
|
||||
if (!versionedTag) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const { name: latestVersion, digest } = versionedTag;
|
||||
const updateAvailable = digest !== currentDigest;
|
||||
|
||||
return { latestVersion, updateAvailable };
|
||||
}
|
||||
const updateAvailable = searchedDigest !== currentDigest;
|
||||
return { latestVersion: imageTag, updateAvailable };
|
||||
};
|
||||
|
||||
interface TreeDataItem {
|
||||
@@ -103,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[] = [];
|
||||
@@ -146,3 +211,35 @@ echo "$json_output"
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const cleanupFullDocker = async (serverId?: string | null) => {
|
||||
const cleanupImages = "docker image prune --force";
|
||||
const cleanupVolumes = "docker volume prune --force";
|
||||
const cleanupContainers = "docker container prune --force";
|
||||
const cleanupSystem = "docker system prune --force --volumes";
|
||||
const cleanupBuilder = "docker builder prune --force";
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`
|
||||
${cleanupImages}
|
||||
${cleanupVolumes}
|
||||
${cleanupContainers}
|
||||
${cleanupSystem}
|
||||
${cleanupBuilder}
|
||||
`,
|
||||
);
|
||||
}
|
||||
await execAsync(`
|
||||
${cleanupImages}
|
||||
${cleanupVolumes}
|
||||
${cleanupContainers}
|
||||
${cleanupSystem}
|
||||
${cleanupBuilder}
|
||||
`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ export const createSshKey = async (input: typeof apiCreateSshKey._type) => {
|
||||
if (!sshKey) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the ssh key",
|
||||
message: "Error creating the SSH Key",
|
||||
});
|
||||
}
|
||||
return sshKey;
|
||||
|
||||
@@ -1,80 +1,54 @@
|
||||
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({
|
||||
accesedProjects: [...user.accesedProjects, 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({
|
||||
accesedServices: [...user.accesedServices, 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 { accesedProjects, canCreateServices } = await findUserByAuthId(userId);
|
||||
const haveAccessToProject = accesedProjects.includes(projectId);
|
||||
const { accessedProjects, canCreateServices } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||
|
||||
if (canCreateServices && haveAccessToProject) {
|
||||
return true;
|
||||
@@ -86,9 +60,10 @@ export const canPerformCreationService = async (
|
||||
export const canPerformAccessService = async (
|
||||
userId: string,
|
||||
serviceId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accesedServices } = await findUserByAuthId(userId);
|
||||
const haveAccessToService = accesedServices.includes(serviceId);
|
||||
const { accessedServices } = await findMemberById(userId, organizationId);
|
||||
const haveAccessToService = accessedServices.includes(serviceId);
|
||||
|
||||
if (haveAccessToService) {
|
||||
return true;
|
||||
@@ -98,11 +73,15 @@ export const canPerformAccessService = async (
|
||||
};
|
||||
|
||||
export const canPeformDeleteService = async (
|
||||
authId: string,
|
||||
userId: string,
|
||||
serviceId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accesedServices, canDeleteServices } = await findUserByAuthId(authId);
|
||||
const haveAccessToService = accesedServices.includes(serviceId);
|
||||
const { accessedServices, canDeleteServices } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
const haveAccessToService = accessedServices.includes(serviceId);
|
||||
|
||||
if (canDeleteServices && haveAccessToService) {
|
||||
return true;
|
||||
@@ -111,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;
|
||||
@@ -121,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;
|
||||
@@ -132,12 +117,13 @@ export const canPerformDeleteProject = async (authId: string) => {
|
||||
};
|
||||
|
||||
export const canPerformAccessProject = async (
|
||||
authId: string,
|
||||
userId: string,
|
||||
projectId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accesedProjects } = await findUserByAuthId(authId);
|
||||
const { accessedProjects } = await findMemberById(userId, organizationId);
|
||||
|
||||
const haveAccessToProject = accesedProjects.includes(projectId);
|
||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||
|
||||
if (haveAccessToProject) {
|
||||
return true;
|
||||
@@ -145,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;
|
||||
@@ -180,6 +185,7 @@ export const checkServiceAccess = async (
|
||||
export const checkProjectAccess = async (
|
||||
authId: string,
|
||||
action: "create" | "delete" | "access",
|
||||
organizationId: string,
|
||||
projectId?: string,
|
||||
) => {
|
||||
let hasPermission = false;
|
||||
@@ -188,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;
|
||||
@@ -206,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;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { db } from "../db";
|
||||
import {
|
||||
type createVolumeBackupSchema,
|
||||
type updateVolumeBackupSchema,
|
||||
volumeBackups,
|
||||
} from "../db/schema";
|
||||
|
||||
export const findVolumeBackupById = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await db.query.volumeBackups.findFirst({
|
||||
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
||||
with: {
|
||||
application: true,
|
||||
postgres: true,
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!volumeBackup) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Volume backup not found",
|
||||
});
|
||||
}
|
||||
|
||||
return volumeBackup;
|
||||
};
|
||||
|
||||
export const createVolumeBackup = async (
|
||||
volumeBackup: z.infer<typeof createVolumeBackupSchema>,
|
||||
) => {
|
||||
const newVolumeBackup = await db
|
||||
.insert(volumeBackups)
|
||||
.values(volumeBackup)
|
||||
.returning()
|
||||
.then((e) => e[0]);
|
||||
|
||||
return newVolumeBackup;
|
||||
};
|
||||
|
||||
export const removeVolumeBackup = async (volumeBackupId: string) => {
|
||||
await db
|
||||
.delete(volumeBackups)
|
||||
.where(eq(volumeBackups.volumeBackupId, volumeBackupId));
|
||||
};
|
||||
|
||||
export const updateVolumeBackup = async (
|
||||
volumeBackupId: string,
|
||||
volumeBackup: z.infer<typeof updateVolumeBackupSchema>,
|
||||
) => {
|
||||
return await db
|
||||
.update(volumeBackups)
|
||||
.set(volumeBackup)
|
||||
.where(eq(volumeBackups.volumeBackupId, volumeBackupId))
|
||||
.returning()
|
||||
.then((e) => e[0]);
|
||||
};
|
||||
@@ -18,6 +18,8 @@ export const setupDirectories = () => {
|
||||
MAIN_TRAEFIK_PATH,
|
||||
MONITORING_PATH,
|
||||
SSH_PATH,
|
||||
SCHEDULES_PATH,
|
||||
VOLUME_BACKUPS_PATH,
|
||||
} = paths();
|
||||
const directories = [
|
||||
BASE_PATH,
|
||||
@@ -28,6 +30,8 @@ export const setupDirectories = () => {
|
||||
SSH_PATH,
|
||||
CERTIFICATES_PATH,
|
||||
MONITORING_PATH,
|
||||
SCHEDULES_PATH,
|
||||
VOLUME_BACKUPS_PATH,
|
||||
];
|
||||
|
||||
for (const dir of directories) {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import type { ContainerCreateOptions } from "dockerode";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findUserById } from "../services/admin";
|
||||
import { getDokployImageTag } from "../services/settings";
|
||||
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
|
||||
export const setupMonitoring = async (serverId: string) => {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
const containerName = "dokploy-monitoring";
|
||||
let imageName = "dokploy/monitoring:latest";
|
||||
|
||||
if (
|
||||
(getDokployImageTag() !== "latest" ||
|
||||
process.env.NODE_ENV === "development") &&
|
||||
!IS_CLOUD
|
||||
) {
|
||||
imageName = "dokploy/monitoring:canary";
|
||||
}
|
||||
|
||||
const settings: ContainerCreateOptions = {
|
||||
name: containerName,
|
||||
Env: [`METRICS_CONFIG=${JSON.stringify(server?.metricsConfig)}`],
|
||||
Image: imageName,
|
||||
HostConfig: {
|
||||
// Memory: 100 * 1024 * 1024, // 100MB en bytes
|
||||
// PidMode: "host",
|
||||
// CapAdd: ["NET_ADMIN", "SYS_ADMIN"],
|
||||
// Privileged: true,
|
||||
RestartPolicy: {
|
||||
Name: "always",
|
||||
},
|
||||
PortBindings: {
|
||||
[`${server.metricsConfig.server.port}/tcp`]: [
|
||||
{
|
||||
HostPort: server.metricsConfig.server.port.toString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
Binds: [
|
||||
"/var/run/docker.sock:/var/run/docker.sock:ro",
|
||||
"/sys:/host/sys:ro",
|
||||
"/etc/os-release:/etc/os-release:ro",
|
||||
"/proc:/host/proc:ro",
|
||||
"/etc/dokploy/monitoring/monitoring.db:/app/monitoring.db",
|
||||
],
|
||||
NetworkMode: "host",
|
||||
},
|
||||
ExposedPorts: {
|
||||
[`${server.metricsConfig.server.port}/tcp`]: {},
|
||||
},
|
||||
};
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
try {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
"mkdir -p /etc/dokploy/monitoring && touch /etc/dokploy/monitoring/monitoring.db",
|
||||
);
|
||||
if (serverId) {
|
||||
await pullRemoteImage(imageName, serverId);
|
||||
}
|
||||
|
||||
// Check if container exists
|
||||
const container = docker.getContainer(containerName);
|
||||
try {
|
||||
await container.inspect();
|
||||
await container.remove({ force: true });
|
||||
console.log("Removed existing container");
|
||||
} catch {
|
||||
// Container doesn't exist, continue
|
||||
}
|
||||
|
||||
await docker.createContainer(settings);
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
await newContainer.start();
|
||||
|
||||
console.log("Monitoring Started ");
|
||||
} catch (error) {
|
||||
console.log("Monitoring Not Found: Starting ", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const setupWebMonitoring = async (userId: string) => {
|
||||
const user = await findUserById(userId);
|
||||
|
||||
const containerName = "dokploy-monitoring";
|
||||
let imageName = "dokploy/monitoring:latest";
|
||||
|
||||
if (
|
||||
(getDokployImageTag() !== "latest" ||
|
||||
process.env.NODE_ENV === "development") &&
|
||||
!IS_CLOUD
|
||||
) {
|
||||
imageName = "dokploy/monitoring:canary";
|
||||
}
|
||||
|
||||
const settings: ContainerCreateOptions = {
|
||||
name: containerName,
|
||||
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
|
||||
Image: imageName,
|
||||
HostConfig: {
|
||||
// Memory: 100 * 1024 * 1024, // 100MB en bytes
|
||||
// PidMode: "host",
|
||||
// CapAdd: ["NET_ADMIN", "SYS_ADMIN"],
|
||||
// Privileged: true,
|
||||
RestartPolicy: {
|
||||
Name: "always",
|
||||
},
|
||||
PortBindings: {
|
||||
[`${user?.metricsConfig?.server?.port}/tcp`]: [
|
||||
{
|
||||
HostPort: user?.metricsConfig?.server?.port.toString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
Binds: [
|
||||
"/var/run/docker.sock:/var/run/docker.sock:ro",
|
||||
"/sys:/host/sys:ro",
|
||||
"/etc/os-release:/etc/os-release:ro",
|
||||
"/proc:/host/proc:ro",
|
||||
"/etc/dokploy/monitoring/monitoring.db:/app/monitoring.db",
|
||||
],
|
||||
// NetworkMode: "host",
|
||||
},
|
||||
ExposedPorts: {
|
||||
[`${user?.metricsConfig?.server?.port}/tcp`]: {},
|
||||
},
|
||||
};
|
||||
const docker = await getRemoteDocker();
|
||||
try {
|
||||
await execAsync(
|
||||
"mkdir -p /etc/dokploy/monitoring && touch /etc/dokploy/monitoring/monitoring.db",
|
||||
);
|
||||
await pullImage(imageName);
|
||||
|
||||
const container = docker.getContainer(containerName);
|
||||
try {
|
||||
await container.inspect();
|
||||
await container.remove({ force: true });
|
||||
console.log("Removed existing container");
|
||||
} catch {}
|
||||
|
||||
await docker.createContainer(settings);
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
await newContainer.start();
|
||||
|
||||
console.log("Monitoring Started ");
|
||||
} catch (error) {
|
||||
console.log("Monitoring Not Found: Starting ", error);
|
||||
}
|
||||
};
|
||||
@@ -54,10 +54,16 @@ export const initializePostgres = async () => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
|
||||
console.log("Postgres Started ✅");
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
} catch (_) {
|
||||
try {
|
||||
await docker.createService(settings);
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode !== 409) {
|
||||
throw error;
|
||||
}
|
||||
console.log("Postgres service already exists, continuing...");
|
||||
}
|
||||
console.log("Postgres Not Found: Starting ✅");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,8 +52,15 @@ export const initializeRedis = async () => {
|
||||
...settings,
|
||||
});
|
||||
console.log("Redis Started ✅");
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
} catch (_) {
|
||||
try {
|
||||
await docker.createService(settings);
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode !== 409) {
|
||||
throw error;
|
||||
}
|
||||
console.log("Redis service already exists, continuing...");
|
||||
}
|
||||
console.log("Redis Not Found: Starting ✅");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Client } from "ssh2";
|
||||
import { findServerById } from "../services/server";
|
||||
|
||||
// Thanks for the idea to https://github.com/healthyhost/audit-vps-script/tree/main
|
||||
const validateUfw = () => `
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
isInstalled=true
|
||||
isActive=$(sudo ufw status | grep -q "Status: active" && echo true || echo false)
|
||||
defaultIncoming=$(sudo ufw status verbose | grep "Default:" | grep "incoming" | awk '{print $2}')
|
||||
echo "{\\"installed\\": $isInstalled, \\"active\\": $isActive, \\"defaultIncoming\\": \\"$defaultIncoming\\"}"
|
||||
else
|
||||
echo "{\\"installed\\": false, \\"active\\": false, \\"defaultIncoming\\": \\"unknown\\"}"
|
||||
fi
|
||||
`;
|
||||
|
||||
const validateSsh = () => `
|
||||
if systemctl is-active --quiet sshd || systemctl is-active --quiet ssh; then
|
||||
isEnabled=true
|
||||
|
||||
# Get the sshd config file path
|
||||
sshd_config=$(sudo sshd -T 2>/dev/null | grep -i "^configfile" | awk '{print $2}')
|
||||
|
||||
# If we couldn't get the path, use the default
|
||||
if [ -z "$sshd_config" ]; then
|
||||
sshd_config="/etc/ssh/sshd_config"
|
||||
fi
|
||||
|
||||
# Check for key authentication
|
||||
# SSH key auth is enabled by default unless explicitly disabled
|
||||
pubkey_line=$(sudo grep -i "^PubkeyAuthentication" "$sshd_config" 2>/dev/null | grep -v "#")
|
||||
if [ -z "$pubkey_line" ] || echo "$pubkey_line" | grep -q -i "yes"; then
|
||||
keyAuth=true
|
||||
else
|
||||
keyAuth=false
|
||||
fi
|
||||
|
||||
# Get the exact PermitRootLogin value from config
|
||||
# This preserves values like "prohibit-password" without normalization
|
||||
permitRootLogin=$(sudo grep -i "^PermitRootLogin" "$sshd_config" 2>/dev/null | grep -v "#" | awk '{print $2}')
|
||||
if [ -z "$permitRootLogin" ]; then
|
||||
# Default is prohibit-password in newer versions
|
||||
permitRootLogin="prohibit-password"
|
||||
fi
|
||||
|
||||
# Get the exact PasswordAuthentication value from config
|
||||
passwordAuth=$(sudo grep -i "^PasswordAuthentication" "$sshd_config" 2>/dev/null | grep -v "#" | awk '{print $2}')
|
||||
if [ -z "$passwordAuth" ]; then
|
||||
# Default is yes
|
||||
passwordAuth="yes"
|
||||
fi
|
||||
|
||||
# Get the exact UsePAM value from config
|
||||
usePam=$(sudo grep -i "^UsePAM" "$sshd_config" 2>/dev/null | grep -v "#" | awk '{print $2}')
|
||||
if [ -z "$usePam" ]; then
|
||||
# Default is yes in most distros
|
||||
usePam="yes"
|
||||
fi
|
||||
|
||||
# Return the results with exact values from config file
|
||||
echo "{\\"enabled\\": $isEnabled, \\"keyAuth\\": $keyAuth, \\"permitRootLogin\\": \\"$permitRootLogin\\", \\"passwordAuth\\": \\"$passwordAuth\\", \\"usePam\\": \\"$usePam\\"}"
|
||||
else
|
||||
echo "{\\"enabled\\": false, \\"keyAuth\\": false, \\"permitRootLogin\\": \\"unknown\\", \\"passwordAuth\\": \\"unknown\\", \\"usePam\\": \\"unknown\\"}"
|
||||
fi
|
||||
`;
|
||||
|
||||
const validateFail2ban = () => `
|
||||
if dpkg -l | grep -q "fail2ban"; then
|
||||
isInstalled=true
|
||||
isEnabled=$(systemctl is-enabled --quiet fail2ban.service && echo true || echo false)
|
||||
isActive=$(systemctl is-active --quiet fail2ban.service && echo true || echo false)
|
||||
|
||||
if [ -f "/etc/fail2ban/jail.local" ]; then
|
||||
sshEnabled=$(grep -A10 "^\\[sshd\\]" /etc/fail2ban/jail.local | grep "enabled" | awk '{print $NF}' | tr -d '[:space:]')
|
||||
sshMode=$(grep -A10 "^\\[sshd\\]" /etc/fail2ban/jail.local | grep "^mode[[:space:]]*=[[:space:]]*aggressive" >/dev/null && echo "aggressive" || echo "normal")
|
||||
echo "{\\"installed\\": $isInstalled, \\"enabled\\": $isEnabled, \\"active\\": $isActive, \\"sshEnabled\\": \\"$sshEnabled\\", \\"sshMode\\": \\"$sshMode\\"}"
|
||||
else
|
||||
echo "{\\"installed\\": $isInstalled, \\"enabled\\": $isEnabled, \\"active\\": $isActive, \\"sshEnabled\\": \\"false\\", \\"sshMode\\": \\"normal\\"}"
|
||||
fi
|
||||
else
|
||||
echo "{\\"installed\\": false, \\"enabled\\": false, \\"active\\": false, \\"sshEnabled\\": \\"false\\", \\"sshMode\\": \\"normal\\"}"
|
||||
fi
|
||||
`;
|
||||
|
||||
export const serverAudit = async (serverId: string) => {
|
||||
const client = new Client();
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId) {
|
||||
throw new Error("No SSH Key found");
|
||||
}
|
||||
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
client
|
||||
.once("ready", () => {
|
||||
const bashCommand = `
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ufwStatus=$(${validateUfw()})
|
||||
sshStatus=$(${validateSsh()})
|
||||
fail2banStatus=$(${validateFail2ban()})
|
||||
|
||||
echo "{\\"ufw\\": $ufwStatus, \\"ssh\\": $sshStatus, \\"fail2ban\\": $fail2banStatus}"
|
||||
`;
|
||||
|
||||
client.exec(bashCommand, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
let output = "";
|
||||
stream
|
||||
.on("close", () => {
|
||||
client.end();
|
||||
try {
|
||||
const result = JSON.parse(output.trim());
|
||||
resolve(result);
|
||||
} catch (parseError) {
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
output += data;
|
||||
})
|
||||
.stderr.on("data", (_data) => {});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
client.end();
|
||||
if (err.level === "client-authentication") {
|
||||
reject(
|
||||
new Error(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
reject(new Error(`SSH connection error: ${err.message}`));
|
||||
}
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import {
|
||||
@@ -7,6 +6,10 @@ import {
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import {
|
||||
TRAEFIK_HTTP3_PORT,
|
||||
TRAEFIK_PORT,
|
||||
TRAEFIK_SSL_PORT,
|
||||
TRAEFIK_VERSION,
|
||||
getDefaultMiddlewares,
|
||||
getDefaultServerTraefikConfig,
|
||||
} from "@dokploy/server/setup/traefik-setup";
|
||||
@@ -29,7 +32,10 @@ export const slugify = (text: string | undefined) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const serverSetup = async (serverId: string) => {
|
||||
export const serverSetup = async (
|
||||
serverId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const server = await findServerById(serverId);
|
||||
const { LOGS_PATH } = paths();
|
||||
|
||||
@@ -45,138 +51,152 @@ export const serverSetup = async (serverId: string) => {
|
||||
description: "Setup Server",
|
||||
});
|
||||
|
||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||
try {
|
||||
writeStream.write("\nInstalling Server Dependencies: ✅\n");
|
||||
await installRequirements(serverId, deployment.logPath);
|
||||
writeStream.close();
|
||||
onData?.("\nInstalling Server Dependencies: ✅\n");
|
||||
await installRequirements(serverId, onData);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
|
||||
onData?.("\nSetup Server: ✅\n");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
writeStream.write(err);
|
||||
writeStream.close();
|
||||
onData?.(`${err} ❌\n`);
|
||||
}
|
||||
};
|
||||
|
||||
const installRequirements = async (serverId: string, logPath: string) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
export const defaultCommand = () => {
|
||||
const bashCommand = `
|
||||
set -e;
|
||||
DOCKER_VERSION=27.0.3
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
SYS_ARCH=$(uname -m)
|
||||
CURRENT_USER=$USER
|
||||
|
||||
echo "Installing requirements for: OS: $OS_TYPE"
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo ❌"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||
OS_TYPE="fedora"
|
||||
fi
|
||||
|
||||
# Check if the OS is popOS, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "pop" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
#Check if the OS is zorin, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "zorin" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||
OS_VERSION="rolling"
|
||||
else
|
||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
dnf install -y findutils >/dev/null
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | opencloudos | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| CPU Architecture | $SYS_ARCH"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
${installUtilities()}
|
||||
|
||||
echo -e "2. Validating ports. "
|
||||
${validatePorts()}
|
||||
|
||||
|
||||
|
||||
echo -e "3. Installing RClone. "
|
||||
${installRClone()}
|
||||
|
||||
echo -e "4. Installing Docker. "
|
||||
${installDocker()}
|
||||
|
||||
echo -e "5. Setting up Docker Swarm"
|
||||
${setupSwarm()}
|
||||
|
||||
echo -e "6. Setting up Network"
|
||||
${setupNetwork()}
|
||||
|
||||
echo -e "7. Setting up Directories"
|
||||
${setupMainDirectory()}
|
||||
${setupDirectories()}
|
||||
|
||||
echo -e "8. Setting up Traefik"
|
||||
${createTraefikConfig()}
|
||||
|
||||
echo -e "9. Setting up Middlewares"
|
||||
${createDefaultMiddlewares()}
|
||||
|
||||
echo -e "10. Setting up Traefik Instance"
|
||||
${createTraefikInstance()}
|
||||
|
||||
echo -e "11. Installing Nixpacks"
|
||||
${installNixpacks()}
|
||||
|
||||
echo -e "12. Installing Buildpacks"
|
||||
${installBuildpacks()}
|
||||
|
||||
echo -e "13. Installing Railpack"
|
||||
${installRailpack()}
|
||||
`;
|
||||
|
||||
return bashCommand;
|
||||
};
|
||||
|
||||
const installRequirements = async (
|
||||
serverId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const client = new Client();
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId) {
|
||||
writeStream.write("❌ No SSH Key found");
|
||||
writeStream.close();
|
||||
onData?.("❌ No SSH Key found, please assign one to this server");
|
||||
throw new Error("No SSH Key found");
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
.once("ready", () => {
|
||||
const bashCommand = `
|
||||
set -e;
|
||||
# Thanks to coolify <3
|
||||
|
||||
DOCKER_VERSION=27.0.3
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
CURRENT_USER=$USER
|
||||
|
||||
echo "Installing requirements for: OS: $OS_TYPE"
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo ❌"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||
OS_TYPE="fedora"
|
||||
fi
|
||||
|
||||
# Check if the OS is popOS, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "pop" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
#Check if the OS is zorin, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "zorin" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||
OS_VERSION="rolling"
|
||||
else
|
||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
${installUtilities()}
|
||||
|
||||
echo -e "2. Validating ports. "
|
||||
${validatePorts()}
|
||||
|
||||
|
||||
|
||||
echo -e "3. Installing RClone. "
|
||||
${installRClone()}
|
||||
|
||||
echo -e "4. Installing Docker. "
|
||||
${installDocker()}
|
||||
|
||||
echo -e "5. Setting up Docker Swarm"
|
||||
${setupSwarm()}
|
||||
|
||||
echo -e "6. Setting up Network"
|
||||
${setupNetwork()}
|
||||
|
||||
echo -e "7. Setting up Directories"
|
||||
${setupMainDirectory()}
|
||||
${setupDirectories()}
|
||||
|
||||
echo -e "8. Setting up Traefik"
|
||||
${createTraefikConfig()}
|
||||
|
||||
echo -e "9. Setting up Middlewares"
|
||||
${createDefaultMiddlewares()}
|
||||
|
||||
echo -e "10. Setting up Traefik Instance"
|
||||
${createTraefikInstance()}
|
||||
|
||||
echo -e "11. Installing Nixpacks"
|
||||
${installNixpacks()}
|
||||
|
||||
echo -e "12. Installing Buildpacks"
|
||||
${installBuildpacks()}
|
||||
`;
|
||||
client.exec(bashCommand, (err, stream) => {
|
||||
const command = server.command || defaultCommand();
|
||||
client.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
writeStream.write(err);
|
||||
onData?.(err.message);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
@@ -186,17 +206,17 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
||||
resolve();
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
writeStream.write(data.toString());
|
||||
onData?.(data.toString());
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
writeStream.write(data.toString());
|
||||
onData?.(data.toString());
|
||||
});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
client.end();
|
||||
if (err.level === "client-authentication") {
|
||||
writeStream.write(
|
||||
onData?.(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
);
|
||||
reject(
|
||||
@@ -205,9 +225,7 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
writeStream.write(
|
||||
`SSH connection error: ${err.message} ${err.level}`,
|
||||
);
|
||||
onData?.(`SSH connection error: ${err.message} ${err.level}`);
|
||||
reject(new Error(`SSH connection error: ${err.message}`));
|
||||
}
|
||||
})
|
||||
@@ -216,7 +234,6 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -246,7 +263,7 @@ const setupMainDirectory = () => `
|
||||
# Create the /etc/dokploy directory
|
||||
mkdir -p /etc/dokploy
|
||||
chmod 777 /etc/dokploy
|
||||
|
||||
|
||||
echo "Directory /etc/dokploy created ✅"
|
||||
fi
|
||||
`;
|
||||
@@ -258,20 +275,49 @@ export const setupSwarm = () => `
|
||||
else
|
||||
# Get IP address
|
||||
get_ip() {
|
||||
# Try to get IPv4
|
||||
local ipv4=\$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||
local ip=""
|
||||
|
||||
if [ -n "\$ipv4" ]; then
|
||||
echo "\$ipv4"
|
||||
else
|
||||
# Try to get IPv6
|
||||
local ipv6=\$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "\$ipv6" ]; then
|
||||
echo "\$ipv6"
|
||||
# Try IPv4 with multiple services
|
||||
# First attempt: ifconfig.io
|
||||
ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
|
||||
|
||||
# Second attempt: icanhazip.com
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Third attempt: ipecho.net
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
|
||||
fi
|
||||
|
||||
# If no IPv4, try IPv6 with multiple services
|
||||
if [ -z "\$ip" ]; then
|
||||
# Try IPv6 with ifconfig.io
|
||||
ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
|
||||
|
||||
# Try IPv6 with icanhazip.com
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Try IPv6 with ipecho.net
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "\$ip" ]; then
|
||||
echo "Error: Could not determine server IP address automatically (neither IPv4 nor IPv6)." >&2
|
||||
echo "Please set the ADVERTISE_ADDR environment variable manually." >&2
|
||||
echo "Example: export ADVERTISE_ADDR=<your-server-ip>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "\$ip"
|
||||
}
|
||||
advertise_addr=\$(get_ip)
|
||||
echo "Advertise address: \$advertise_addr"
|
||||
|
||||
# Initialize Docker Swarm
|
||||
docker swarm init --advertise-addr \$advertise_addr
|
||||
@@ -310,20 +356,20 @@ const installUtilities = () => `
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git jq openssl >/dev/null
|
||||
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl wget git jq openssl >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
centos | fedora | rhel | ol | rocky | almalinux | opencloudos | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
dnf install -y wget git git-lfs jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
@@ -331,12 +377,12 @@ const installUtilities = () => `
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl unzip >/dev/null
|
||||
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git jq openssl >/dev/null
|
||||
zypper install -y curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
@@ -372,6 +418,28 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"opencloudos")
|
||||
# Special handling for OpenCloud OS
|
||||
echo " - Installing Docker for OpenCloud OS..."
|
||||
dnf install -y docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove --live-restore parameter from Docker configuration if it exists
|
||||
if [ -f "/etc/sysconfig/docker" ]; then
|
||||
echo " - Removing --live-restore parameter from Docker configuration..."
|
||||
sed -i 's/--live-restore[^[:space:]]*//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
sed -i 's/--live-restore//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
# Clean up any double spaces that might be left
|
||||
sed -i 's/ */ /g' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
echo " - Docker configured for OpenCloud OS"
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
@@ -489,29 +557,38 @@ export const installRClone = () => `
|
||||
echo "RClone already installed ✅"
|
||||
else
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
echo "RClone installed successfully ✅"
|
||||
RCLONE_VERSION=$(rclone --version | head -n 1 | awk '{print $2}' | sed 's/^v//')
|
||||
echo "RClone version $RCLONE_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
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 8
|
||||
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
|
||||
docker service create \
|
||||
--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=443,published=443 \
|
||||
--publish mode=host,target=80,published=80 \
|
||||
traefik:v3.1.2
|
||||
# Create the dokploy-traefik container
|
||||
TRAEFIK_VERSION=${TRAEFIK_VERSION}
|
||||
docker run -d \
|
||||
--name dokploy-traefik \
|
||||
--network dokploy-network \
|
||||
--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
|
||||
`;
|
||||
|
||||
@@ -522,16 +599,32 @@ const installNixpacks = () => `
|
||||
if command_exists nixpacks; then
|
||||
echo "Nixpacks already installed ✅"
|
||||
else
|
||||
VERSION=1.28.1 bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version 1.28.1 installed ✅"
|
||||
export NIXPACKS_VERSION=1.39.0
|
||||
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
const installRailpack = () => `
|
||||
if command_exists railpack; then
|
||||
echo "Railpack already installed ✅"
|
||||
else
|
||||
export RAILPACK_VERSION=0.0.64
|
||||
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
|
||||
SUFFIX="-arm64"
|
||||
fi
|
||||
if command_exists pack; then
|
||||
echo "Buildpacks already installed ✅"
|
||||
else
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
echo "Buildpacks version 0.35.0 installed ✅"
|
||||
BUILDPACKS_VERSION=0.35.0
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
@@ -11,7 +11,7 @@ export const validateDocker = () => `
|
||||
|
||||
export const validateRClone = () => `
|
||||
if command_exists rclone; then
|
||||
echo "$(rclone --version | head -n 1 | awk '{print $2}') true"
|
||||
echo "$(rclone --version | head -n 1 | awk '{print $2}' | sed 's/^v//') true"
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
@@ -27,15 +27,37 @@ export const validateSwarm = () => `
|
||||
|
||||
export const validateNixpacks = () => `
|
||||
if command_exists nixpacks; then
|
||||
echo "$(nixpacks --version | awk '{print $2}') true"
|
||||
version=$(nixpacks --version | awk '{print $2}')
|
||||
if [ -n "$version" ]; then
|
||||
echo "$version true"
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
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
|
||||
echo "$(pack --version | awk '{print $1}') true"
|
||||
version=$(pack --version | awk '{print $1}')
|
||||
if [ -n "$version" ]; then
|
||||
echo "$version true"
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
@@ -76,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}')
|
||||
|
||||
@@ -86,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}')
|
||||
|
||||
@@ -93,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) {
|
||||
@@ -118,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) => {
|
||||
@@ -138,7 +163,6 @@ export const serverValidate = async (serverId: string) => {
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export const dockerSwarmInitialized = async () => {
|
||||
await docker.swarmInspect();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -41,7 +41,7 @@ export const dockerNetworkInitialized = async () => {
|
||||
try {
|
||||
await docker.getNetwork("dokploy-network").inspect();
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,125 +1,151 @@
|
||||
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";
|
||||
|
||||
const TRAEFIK_SSL_PORT =
|
||||
export const TRAEFIK_SSL_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_SSL_PORT!, 10) || 443;
|
||||
const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||
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 {
|
||||
enableDashboard?: boolean;
|
||||
env?: string[];
|
||||
serverId?: string;
|
||||
additionalPorts?: {
|
||||
targetPort: number;
|
||||
publishedPort: number;
|
||||
}[];
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export const initializeTraefik = async ({
|
||||
enableDashboard = false,
|
||||
env,
|
||||
serverId,
|
||||
additionalPorts = [],
|
||||
force = false,
|
||||
}: TraefikOptions = {}) => {
|
||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||
const imageName = "traefik:v3.1.2";
|
||||
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,
|
||||
},
|
||||
},
|
||||
Labels: {
|
||||
"traefik.enable": "true",
|
||||
},
|
||||
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,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
|
||||
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);
|
||||
try {
|
||||
const service = docker.getService("dokploy-traefik");
|
||||
await service?.remove({ force: true });
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 5;
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
await docker.listServices({
|
||||
filters: { name: ["dokploy-traefik"] },
|
||||
});
|
||||
console.log("Waiting for service cleanup...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
attempts++;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.log("No existing service to remove");
|
||||
}
|
||||
|
||||
const service = docker.getService(containerName);
|
||||
const inspect = await service.inspect();
|
||||
// Then try to remove any existing container
|
||||
const container = docker.getContainer(containerName);
|
||||
try {
|
||||
const inspect = await container.inspect();
|
||||
if (inspect.State.Status === "running" && !force) {
|
||||
console.log("Traefik already running");
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEnv = inspect.Spec.TaskTemplate.ContainerSpec.Env || [];
|
||||
const updatedEnv = !env ? existingEnv : env;
|
||||
await container.remove({ force: true });
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
} catch {
|
||||
console.log("No existing container to remove");
|
||||
}
|
||||
|
||||
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 ✅");
|
||||
// Create and start the new container
|
||||
try {
|
||||
await docker.createContainer(settings);
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
await newContainer.start();
|
||||
console.log("Traefik container started successfully");
|
||||
} catch (error: any) {
|
||||
if (error?.json?.message?.includes("port is already allocated")) {
|
||||
console.log("Ports still in use, waiting longer for cleanup...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
// Try one more time
|
||||
await docker.createContainer(settings);
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
await newContainer.start();
|
||||
console.log("Traefik container started successfully after retry");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await docker.createService(settings);
|
||||
console.log("Traefik Not Found: Starting ✅");
|
||||
console.error("Failed to initialize Traefik:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,6 +191,9 @@ export const createDefaultServerTraefikConfig = () => {
|
||||
|
||||
export const getDefaultTraefikConfig = () => {
|
||||
const configObject: MainTraefikConfig = {
|
||||
global: {
|
||||
sendAnonymousUsage: false,
|
||||
},
|
||||
providers: {
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? {
|
||||
@@ -176,10 +205,12 @@ export const getDefaultTraefikConfig = () => {
|
||||
: {
|
||||
swarm: {
|
||||
exposedByDefault: false,
|
||||
watch: false,
|
||||
watch: true,
|
||||
},
|
||||
docker: {
|
||||
exposedByDefault: false,
|
||||
watch: true,
|
||||
network: "dokploy-network",
|
||||
},
|
||||
}),
|
||||
file: {
|
||||
@@ -193,6 +224,9 @@ export const getDefaultTraefikConfig = () => {
|
||||
},
|
||||
websecure: {
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
http3: {
|
||||
advertisedPort: TRAEFIK_HTTP3_PORT,
|
||||
},
|
||||
...(process.env.NODE_ENV === "production" && {
|
||||
http: {
|
||||
tls: {
|
||||
@@ -230,10 +264,12 @@ export const getDefaultServerTraefikConfig = () => {
|
||||
providers: {
|
||||
swarm: {
|
||||
exposedByDefault: false,
|
||||
watch: false,
|
||||
watch: true,
|
||||
},
|
||||
docker: {
|
||||
exposedByDefault: false,
|
||||
watch: true,
|
||||
network: "dokploy-network",
|
||||
},
|
||||
file: {
|
||||
directory: "/etc/dokploy/traefik/dynamic",
|
||||
@@ -246,6 +282,9 @@ export const getDefaultServerTraefikConfig = () => {
|
||||
},
|
||||
websecure: {
|
||||
address: `:${TRAEFIK_SSL_PORT}`,
|
||||
http3: {
|
||||
advertisedPort: TRAEFIK_HTTP3_PORT,
|
||||
},
|
||||
http: {
|
||||
tls: {
|
||||
certResolver: "letsencrypt",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user