fix: resolve merge conflicts with upstream/canary

This commit is contained in:
vishalkadam47
2024-11-03 05:16:55 +05:30
535 changed files with 13051 additions and 83679 deletions

View File

@@ -29,6 +29,7 @@ import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings";
import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe";
import { userRouter } from "./routers/user";
/**
@@ -69,6 +70,7 @@ export const appRouter = createTRPCRouter({
gitlab: gitlabRouter,
github: githubRouter,
server: serverRouter,
stripe: stripeRouter,
});
// export type definition of API

View File

@@ -6,7 +6,6 @@ import {
apiRemoveUser,
users,
} from "@/server/db/schema";
import {
createInvitation,
findAdminById,

View File

@@ -19,11 +19,8 @@ import {
apiUpdateApplication,
applications,
} from "@/server/db/schema";
import {
type DeploymentJob,
cleanQueuesByApplication,
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
import {

View File

@@ -4,11 +4,13 @@ import {
apiFindOneAuth,
apiLogin,
apiUpdateAuth,
apiUpdateAuthByAdmin,
apiVerify2FA,
apiVerifyLogin2FA,
auth,
} from "@/server/db/schema";
import { WEBSITE_URL } from "@/server/utils/stripe";
import {
type Auth,
IS_CLOUD,
createAdmin,
createUser,
@@ -18,12 +20,18 @@ import {
getUserByToken,
lucia,
luciaToken,
sendDiscordNotification,
sendEmailNotification,
updateAuthById,
validateRequest,
verify2FA,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
import { db } from "../../db";
import {
adminProcedure,
@@ -47,6 +55,12 @@ export const authRouter = createTRPCRouter({
}
}
const newAdmin = await createAdmin(input);
if (IS_CLOUD) {
await sendDiscordNotificationWelcome(newAdmin);
await sendVerificationEmail(newAdmin.id);
return true;
}
const session = await lucia.createSession(newAdmin.id || "", {});
ctx.res.appendHeader(
"Set-Cookie",
@@ -54,7 +68,12 @@ export const authRouter = createTRPCRouter({
);
return true;
} catch (error) {
throw error;
throw new TRPCError({
code: "BAD_REQUEST",
// @ts-ignore
message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error to create admin"}`,
cause: error,
});
}
}),
createUser: publicProcedure
@@ -68,7 +87,13 @@ export const authRouter = createTRPCRouter({
message: "Invalid token",
});
}
const newUser = await createUser(input);
if (IS_CLOUD) {
await sendVerificationEmail(token.authId);
return true;
}
const session = await lucia.createSession(newUser?.authId || "", {});
ctx.res.appendHeader(
"Set-Cookie",
@@ -100,6 +125,15 @@ export const authRouter = createTRPCRouter({
});
}
if (auth?.confirmationToken && IS_CLOUD) {
await sendVerificationEmail(auth.id);
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Email not confirmed, we have sent you a confirmation email please check your inbox.",
});
}
if (auth?.is2FAEnabled) {
return {
is2FAEnabled: true,
@@ -120,7 +154,7 @@ export const authRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Credentials do not match",
message: `Error: ${error instanceof Error ? error.message : "Error to login"}`,
cause: error,
});
}
@@ -145,7 +179,7 @@ export const authRouter = createTRPCRouter({
.input(apiUpdateAuth)
.mutation(async ({ ctx, input }) => {
const auth = await updateAuthById(ctx.user.authId, {
...(input.email && { email: input.email }),
...(input.email && { email: input.email.toLowerCase() }),
...(input.password && {
password: bcrypt.hashSync(input.password, 10),
}),
@@ -177,19 +211,6 @@ export const authRouter = createTRPCRouter({
return auth;
}),
updateByAdmin: protectedProcedure
.input(apiUpdateAuthByAdmin)
.mutation(async ({ input }) => {
const auth = await updateAuthById(input.id, {
...(input.email && { email: input.email }),
...(input.password && {
password: bcrypt.hashSync(input.password, 10),
}),
...(input.image && { image: input.image }),
});
return auth;
}),
generate2FASecret: protectedProcedure.query(async ({ ctx }) => {
return await generate2FASecret(ctx.user.authId);
}),
@@ -230,7 +251,208 @@ export const authRouter = createTRPCRouter({
});
return auth;
}),
verifyToken: protectedProcedure.mutation(async () => {
return true;
}),
sendResetPasswordEmail: publicProcedure
.input(
z.object({
email: z.string().min(1).email(),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "This feature is only available in the cloud version",
});
}
const authR = await db.query.auth.findFirst({
where: eq(auth.email, input.email),
});
if (!authR) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const token = nanoid();
await updateAuthById(authR.id, {
resetPasswordToken: token,
// Make resetPassword in 24 hours
resetPasswordExpiresAt: new Date(
new Date().getTime() + 24 * 60 * 60 * 1000,
).toISOString(),
});
await sendEmailNotification(
{
fromAddress: process.env.SMTP_FROM_ADDRESS!,
toAddresses: [authR.email],
smtpServer: process.env.SMTP_SERVER!,
smtpPort: Number(process.env.SMTP_PORT),
username: process.env.SMTP_USERNAME!,
password: process.env.SMTP_PASSWORD!,
},
"Reset Password",
`
Reset your password by clicking the link below:
The link will expire in 24 hours.
<a href="${WEBSITE_URL}/reset-password?token=${token}">
Reset Password
</a>
`,
);
}),
resetPassword: publicProcedure
.input(
z.object({
resetPasswordToken: z.string().min(1),
password: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "This feature is only available in the cloud version",
});
}
const authR = await db.query.auth.findFirst({
where: eq(auth.resetPasswordToken, input.resetPasswordToken),
});
if (!authR || authR.resetPasswordExpiresAt === null) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Token not found",
});
}
const isExpired = isBefore(
new Date(authR.resetPasswordExpiresAt),
new Date(),
);
if (isExpired) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Token expired",
});
}
await updateAuthById(authR.id, {
resetPasswordExpiresAt: null,
resetPasswordToken: null,
password: bcrypt.hashSync(input.password, 10),
});
return true;
}),
confirmEmail: adminProcedure
.input(
z.object({
confirmationToken: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Functionality not available in cloud version",
});
}
const authR = await db.query.auth.findFirst({
where: eq(auth.confirmationToken, input.confirmationToken),
});
if (!authR || authR.confirmationExpiresAt === null) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Token not found",
});
}
if (authR.confirmationToken !== input.confirmationToken) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Confirmation Token not found",
});
}
const isExpired = isBefore(
new Date(authR.confirmationExpiresAt),
new Date(),
);
if (isExpired) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Confirmation Token expired",
});
}
1;
await updateAuthById(authR.id, {
confirmationToken: null,
confirmationExpiresAt: null,
});
return true;
}),
});
export const sendVerificationEmail = async (authId: string) => {
const token = nanoid();
const result = await updateAuthById(authId, {
confirmationToken: token,
confirmationExpiresAt: new Date(
new Date().getTime() + 24 * 60 * 60 * 1000,
).toISOString(),
});
if (!result) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "User not found",
});
}
await sendEmailNotification(
{
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
toAddresses: [result?.email],
smtpServer: process.env.SMTP_SERVER || "",
smtpPort: Number(process.env.SMTP_PORT),
username: process.env.SMTP_USERNAME || "",
password: process.env.SMTP_PASSWORD || "",
},
"Confirm your email | Dokploy",
`
Welcome to Dokploy!
Please confirm your email by clicking the link below:
<a href="${WEBSITE_URL}/confirm-email?token=${result?.confirmationToken}">
Confirm Email
</a>
`,
);
return true;
};
export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
await sendDiscordNotification(
{
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
},
{
title: "✅ New User Registered",
color: 0x00ff00,
fields: [
{
name: "Email",
value: newAdmin.email,
inline: true,
},
],
timestamp: newAdmin.createdAt,
footer: {
text: "Dokploy User Registration Notification",
},
},
);
};

View File

@@ -14,6 +14,7 @@ import {
findMongoByBackupId,
findMySqlByBackupId,
findPostgresByBackupId,
findServerById,
removeBackupById,
removeScheduleBackup,
runMariadbBackup,
@@ -36,6 +37,25 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(newBackup.backupId);
if (IS_CLOUD && backup.enabled) {
const databaseType = backup.databaseType;
let serverId = "";
if (databaseType === "postgres" && backup.postgres?.serverId) {
serverId = backup.postgres.serverId;
} else if (databaseType === "mysql" && backup.mysql?.serverId) {
serverId = backup.mysql.serverId;
} else if (databaseType === "mongo" && backup.mongo?.serverId) {
serverId = backup.mongo.serverId;
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
serverId = backup.mariadb.serverId;
}
const server = await findServerById(serverId);
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
await schedule({
cronSchedule: backup.schedule,
backupId: backup.backupId,

View File

@@ -9,11 +9,7 @@ import {
apiUpdateCompose,
compose,
} from "@/server/db/schema";
import {
type DeploymentJob,
cleanQueuesByCompose,
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import {
@@ -28,6 +24,7 @@ import _ from "lodash";
import { nanoid } from "nanoid";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { deploy } from "@/server/utils/deploy";
import {
IS_CLOUD,
@@ -41,7 +38,6 @@ import {
createComposeByTemplate,
createDomain,
createMount,
findAdmin,
findAdminById,
findComposeById,
findDomainsByComposeId,
@@ -252,7 +248,6 @@ export const composeRouter = createTRPCRouter({
descriptionLog: "",
server: !!compose.serverId,
};
console.log(jobData);
if (IS_CLOUD && compose.serverId) {
jobData.serverId = compose.serverId;
@@ -362,15 +357,7 @@ export const composeRouter = createTRPCRouter({
const generate = await loadTemplateModule(input.id as TemplatesKeys);
const admin = await findAdminById(ctx.user.adminId);
let serverIp = admin.serverIp;
if (!admin.serverIp) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"You need to have a server IP to deploy this template in order to generate domains",
});
}
let serverIp = admin.serverIp || "127.0.0.1";
const project = await findProjectById(input.projectId);

View File

@@ -12,9 +12,10 @@ import {
destinations,
} from "@/server/db/schema";
import {
IS_CLOUD,
createDestintation,
execAsync,
findAdmin,
execAsyncRemote,
findDestinationById,
removeDestinationById,
updateDestinationById,
@@ -53,11 +54,26 @@ export const destinationRouter = createTRPCRouter({
];
const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
await execAsync(rcloneCommand);
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (IS_CLOUD) {
await execAsyncRemote(input.serverId || "", rcloneCommand);
} else {
await execAsync(rcloneCommand);
}
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to connect to bucket",
message:
error instanceof Error
? error?.message
: "Error to connect to bucket",
cause: error,
});
}

View File

@@ -18,6 +18,7 @@ import {
deployMariadb,
findMariadbById,
findProjectById,
findServerById,
removeMariadbById,
removeService,
startService,
@@ -151,6 +152,7 @@ export const mariadbRouter = createTRPCRouter({
message: "You are not authorized to deploy this mariadb",
});
}
return deployMariadb(input.mariadbId);
}),
changeStatus: protectedProcedure

View File

@@ -148,12 +148,6 @@ export const notificationRouter = createTRPCRouter({
.input(apiCreateDiscord)
.mutation(async ({ input, ctx }) => {
try {
// go to your discord server
// go to settings
// go to integrations
// add a new integration
// select webhook
// copy the webhook url
return await createDiscordNotification(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({

View File

@@ -20,10 +20,12 @@ import { and, desc, eq, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
import {
IS_CLOUD,
addNewProject,
checkProjectAccess,
createProject,
deleteProject,
findAdminById,
findProjectById,
findUserByAuthId,
updateProjectById,
@@ -38,6 +40,15 @@ export const projectRouter = createTRPCRouter({
await checkProjectAccess(ctx.user.authId, "create");
}
const admin = await findAdminById(ctx.user.adminId);
if (admin.serversQuantity === 0 && IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "No servers available, Please subscribe to a plan",
});
}
const project = await createProject(input, ctx.user.adminId);
if (ctx.user.rol === "user") {
await addNewProject(ctx.user.authId, project.projectId);
@@ -47,7 +58,7 @@ export const projectRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the project",
message: `Error to create the project: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
@@ -121,11 +132,15 @@ export const projectRouter = createTRPCRouter({
if (accesedProjects.length === 0) {
return [];
}
const query = await db.query.projects.findMany({
where: sql`${projects.projectId} IN (${sql.join(
accesedProjects.map((projectId) => sql`${projectId}`),
sql`, `,
)})`,
where: and(
sql`${projects.projectId} IN (${sql.join(
accesedProjects.map((projectId) => sql`${projectId}`),
sql`, `,
)})`,
eq(projects.adminId, ctx.user.adminId),
),
with: {
applications: {
where: buildServiceFilter(
@@ -230,5 +245,5 @@ function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) {
accesedServices.map((serviceId) => sql`${serviceId}`),
sql`, `,
)})`
: sql`1 = 0`; // Always false condition
: sql`1 = 0`;
}

View File

@@ -1,6 +1,5 @@
import {
apiCreateRegistry,
apiEnableSelfHostedRegistry,
apiFindOneRegistry,
apiRemoveRegistry,
apiTestRegistry,
@@ -13,8 +12,6 @@ import {
execAsyncRemote,
findAllRegistryByAdminId,
findRegistryById,
initializeRegistry,
manageRegistry,
removeRegistry,
updateRegistry,
} from "@dokploy/server";
@@ -84,6 +81,13 @@ export const registryRouter = createTRPCRouter({
try {
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Select a server to test the registry",
});
}
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else {
@@ -96,34 +100,4 @@ export const registryRouter = createTRPCRouter({
return false;
}
}),
enableSelfHostedRegistry: adminProcedure
.input(apiEnableSelfHostedRegistry)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Self Hosted Registry is not available in the cloud version",
});
}
const selfHostedRegistry = await createRegistry(
{
...input,
registryName: "Self Hosted Registry",
registryType: "selfHosted",
registryUrl:
process.env.NODE_ENV === "production"
? input.registryUrl
: "dokploy-registry.docker.localhost",
imagePrefix: null,
serverId: undefined,
},
ctx.user.adminId,
);
await manageRegistry(selfHostedRegistry);
await initializeRegistry(input.username, input.password);
return selfHostedRegistry;
}),
});

View File

@@ -1,3 +1,4 @@
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
@@ -15,15 +16,17 @@ import {
server,
} from "@/server/db/schema";
import {
IS_CLOUD,
createServer,
deleteServer,
findAdminById,
findServerById,
findServersByAdminId,
haveActiveServices,
removeDeploymentsByServerId,
serverSetup,
updateServerById,
} from "@dokploy/server";
// import { serverSetup } from "@/server/setup/server-setup";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
@@ -32,6 +35,14 @@ export const serverRouter = createTRPCRouter({
.input(apiCreateServer)
.mutation(async ({ ctx, input }) => {
try {
const admin = await findAdminById(ctx.user.adminId);
const servers = await findServersByAdminId(admin.adminId);
if (IS_CLOUD && servers.length >= admin.serversQuantity) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You cannot create more servers",
});
}
const project = await createServer(input, ctx.user.adminId);
return project;
} catch (error) {
@@ -77,13 +88,17 @@ export const serverRouter = createTRPCRouter({
return result;
}),
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
return await db.query.server.findMany({
const result = await db.query.server.findMany({
orderBy: desc(server.createdAt),
where: and(
isNotNull(server.sshKeyId),
eq(server.adminId, ctx.user.adminId),
),
where: IS_CLOUD
? and(
isNotNull(server.sshKeyId),
eq(server.adminId, ctx.user.adminId),
eq(server.serverStatus, "active"),
)
: and(isNotNull(server.sshKeyId), eq(server.adminId, ctx.user.adminId)),
});
return result;
}),
setup: protectedProcedure
.input(apiFindOneServer)
@@ -125,6 +140,15 @@ export const serverRouter = createTRPCRouter({
await removeDeploymentsByServerId(currentServer);
await deleteServer(input.serverId);
if (IS_CLOUD) {
const admin = await findAdminById(ctx.user.adminId);
await updateServersBasedOnQuantity(
admin.adminId,
admin.serversQuantity,
);
}
return currentServer;
} catch (error) {
throw error;
@@ -141,6 +165,13 @@ export const serverRouter = createTRPCRouter({
message: "You are not authorized to update this server",
});
}
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
const currentServer = await updateServerById(input.serverId, {
...input,
});

View File

@@ -225,6 +225,13 @@ export const settingsRouter = createTRPCRouter({
}
if (server.enableDockerCleanup) {
const server = await findServerById(input.serverId);
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
if (IS_CLOUD) {
await schedule({
cronSchedule: "0 0 * * *",
@@ -507,7 +514,7 @@ export const settingsRouter = createTRPCRouter({
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else {
} else if (!IS_CLOUD) {
const result = await execAsync(
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
);
@@ -639,7 +646,7 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
isCloud: adminProcedure.query(async () => {
isCloud: protectedProcedure.query(async () => {
return IS_CLOUD;
}),
health: publicProcedure.query(async () => {

View File

@@ -0,0 +1,131 @@
import { WEBSITE_URL, getStripeItems } from "@/server/utils/stripe";
import {
IS_CLOUD,
findAdminById,
findServersByAdminId,
updateAdmin,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import Stripe from "stripe";
import { z } from "zod";
import { adminProcedure, createTRPCRouter } from "../trpc";
export const stripeRouter = createTRPCRouter({
getProducts: adminProcedure.query(async ({ ctx }) => {
const admin = await findAdminById(ctx.user.adminId);
const stripeCustomerId = admin.stripeCustomerId;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});
const products = await stripe.products.list({
expand: ["data.default_price"],
active: true,
});
if (!stripeCustomerId) {
return {
products: products.data,
subscriptions: [],
};
}
const subscriptions = await stripe.subscriptions.list({
customer: stripeCustomerId,
status: "active",
expand: ["data.items.data.price"],
});
return {
products: products.data,
subscriptions: subscriptions.data,
};
}),
createCheckoutSession: adminProcedure
.input(
z.object({
productId: z.string(),
serverQuantity: z.number().min(1),
isAnnual: z.boolean(),
}),
)
.mutation(async ({ ctx, input }) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});
const items = getStripeItems(input.serverQuantity, input.isAnnual);
const admin = await findAdminById(ctx.user.adminId);
let stripeCustomerId = admin.stripeCustomerId;
if (stripeCustomerId) {
const customer = await stripe.customers.retrieve(stripeCustomerId);
if (customer.deleted) {
await updateAdmin(admin.authId, {
stripeCustomerId: null,
});
stripeCustomerId = null;
}
}
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: items,
...(stripeCustomerId && {
customer: stripeCustomerId,
}),
metadata: {
adminId: admin.adminId,
},
allow_promotion_codes: true,
success_url: `${WEBSITE_URL}/dashboard/settings/billing`,
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
});
return { sessionId: session.id };
}),
createCustomerPortalSession: adminProcedure.mutation(
async ({ ctx, input }) => {
const admin = await findAdminById(ctx.user.adminId);
if (!admin.stripeCustomerId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Stripe Customer ID not found",
});
}
const stripeCustomerId = admin.stripeCustomerId;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});
try {
const session = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: `${WEBSITE_URL}/dashboard/settings/billing`,
});
return { url: session.url };
} catch (error) {
return {
url: "",
};
}
},
),
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
const admin = await findAdminById(ctx.user.adminId);
const servers = await findServersByAdminId(admin.adminId);
if (!IS_CLOUD) {
return true;
}
return servers.length < admin.serversQuantity;
}),
});

View File

@@ -4,7 +4,7 @@ export default defineConfig({
schema: "./server/db/schema/index.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL || "",
url: process.env.DATABASE_URL!,
},
out: "drizzle",
migrations: {

View File

@@ -8,12 +8,12 @@ declare global {
export let db: PostgresJsDatabase<typeof schema>;
if (process.env.NODE_ENV === "production") {
db = drizzle(postgres(process.env.DATABASE_URL || ""), {
db = drizzle(postgres(process.env.DATABASE_URL!), {
schema,
});
} else {
if (!global.db)
global.db = drizzle(postgres(process.env.DATABASE_URL || ""), {
global.db = drizzle(postgres(process.env.DATABASE_URL!), {
schema,
});

View File

@@ -2,7 +2,7 @@ import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL || "";
const connectionString = process.env.DATABASE_URL!;
const sql = postgres(connectionString, { max: 1 });
const db = drizzle(sql);

View File

@@ -3,7 +3,7 @@ import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL || "";
const connectionString = process.env.DATABASE_URL!;
const pg = postgres(connectionString, { max: 1 });
const db = drizzle(pg);

View File

@@ -1 +1 @@
export * from "@dokploy/server/dist/db/schema";
export * from "@dokploy/server/db/schema";

View File

@@ -3,7 +3,7 @@ import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { users } from "./schema";
const connectionString = process.env.DATABASE_URL || "";
const connectionString = process.env.DATABASE_URL!;
const pg = postgres(connectionString, { max: 1 });
const db = drizzle(pg);

View File

@@ -11,29 +11,8 @@ import {
updateCompose,
} from "@dokploy/server";
import { type Job, Worker } from "bullmq";
import { myQueue, redisConfig } from "./queueSetup";
type DeployJob =
| {
applicationId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "application";
serverId?: string;
}
| {
composeId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "compose";
serverId?: string;
};
export type DeploymentJob = DeployJob;
import type { DeploymentJob } from "./queue-types";
import { redisConfig } from "./redis-connection";
export const deploymentWorker = new Worker(
"deployments",
@@ -114,25 +93,3 @@ export const deploymentWorker = new Worker(
connection: redisConfig,
},
);
export const cleanQueuesByApplication = async (applicationId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.applicationId === applicationId) {
await job.remove();
console.log(`Removed job ${job.id} for application ${applicationId}`);
}
}
};
export const cleanQueuesByCompose = async (composeId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.composeId === composeId) {
await job.remove();
console.log(`Removed job ${job.id} for compose ${composeId}`);
}
}
};

View File

@@ -0,0 +1,21 @@
type DeployJob =
| {
applicationId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "application";
serverId?: string;
}
| {
composeId: string;
titleLog: string;
descriptionLog: string;
server?: boolean;
type: "deploy" | "redeploy";
applicationType: "compose";
serverId?: string;
};
export type DeploymentJob = DeployJob;

View File

@@ -1,8 +1,6 @@
import { type ConnectionOptions, Queue } from "bullmq";
import { Queue } from "bullmq";
import { redisConfig } from "./redis-connection";
export const redisConfig: ConnectionOptions = {
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
};
const myQueue = new Queue("deployments", {
connection: redisConfig,
});
@@ -21,4 +19,26 @@ myQueue.on("error", (error) => {
}
});
export const cleanQueuesByApplication = async (applicationId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.applicationId === applicationId) {
await job.remove();
console.log(`Removed job ${job.id} for application ${applicationId}`);
}
}
};
export const cleanQueuesByCompose = async (composeId: string) => {
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
for (const job of jobs) {
if (job?.data?.composeId === composeId) {
await job.remove();
console.log(`Removed job ${job.id} for compose ${composeId}`);
}
}
};
export { myQueue };

View File

@@ -0,0 +1,5 @@
import type { ConnectionOptions } from "bullmq";
export const redisConfig: ConnectionOptions = {
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
};

View File

@@ -19,15 +19,12 @@ import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
import {
getPublicIpWithFallback,
setupTerminalWebSocketServer,
} from "./wss/terminal";
import { setupTerminalWebSocketServer } from "./wss/terminal";
config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const app = next({ dev, turbopack: dev });
const handle = app.getRequestHandler();
void app.prepare().then(async () => {
try {
@@ -40,7 +37,9 @@ void app.prepare().then(async () => {
setupDockerContainerLogsWebSocketServer(server);
setupDockerContainerTerminalWebSocketServer(server);
setupTerminalWebSocketServer(server);
setupDockerStatsMonitoringSocketServer(server);
if (!IS_CLOUD) {
setupDockerStatsMonitoringSocketServer(server);
}
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
setupDirectories();
@@ -53,7 +52,6 @@ void app.prepare().then(async () => {
await initializeRedis();
initCronJobs();
welcomeServer();
// Timeout to wait for the database to be ready
await new Promise((resolve) => setTimeout(resolve, 7000));
@@ -76,18 +74,3 @@ void app.prepare().then(async () => {
console.error("Main Server Error", e);
}
});
async function welcomeServer() {
const ip = await getPublicIpWithFallback();
console.log(
[
"",
"",
"Dokploy server is up and running!",
"Please wait for 15 seconds before opening the browser.",
` http://${ip}:${PORT}`,
"",
"",
].join("\n"),
);
}

View File

@@ -20,10 +20,8 @@ export const schedule = async (job: QueueJob) => {
body: JSON.stringify(job),
});
const data = await result.json();
console.log(data);
return data;
} catch (error) {
console.log(error);
throw error;
}
};
@@ -39,10 +37,8 @@ export const removeJob = async (job: QueueJob) => {
body: JSON.stringify(job),
});
const data = await result.json();
console.log(data);
return data;
} catch (error) {
console.log(error);
throw error;
}
};
@@ -58,10 +54,8 @@ export const updateJob = async (job: QueueJob) => {
body: JSON.stringify(job),
});
const data = await result.json();
console.log(data);
return data;
} catch (error) {
console.log(error);
throw error;
}
};

View File

@@ -1,7 +1,13 @@
import type { DeploymentJob } from "../queues/deployments-queue";
import { findServerById } from "@dokploy/server";
import type { DeploymentJob } from "../queues/queue-types";
export const deploy = async (jobData: DeploymentJob) => {
try {
const server = await findServerById(jobData.serverId as string);
if (server.serverStatus === "inactive") {
throw new Error("Server is inactive");
}
const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
method: "POST",
headers: {
@@ -10,11 +16,10 @@ export const deploy = async (jobData: DeploymentJob) => {
},
body: JSON.stringify(jobData),
});
const data = await result.json();
console.log(data);
return data;
} catch (error) {
console.log(error);
throw error;
}
};

View File

@@ -0,0 +1,27 @@
export const WEBSITE_URL =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: process.env.SITE_URL;
const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID!; // $4.00
const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID!; // $7.99
export const getStripeItems = (serverQuantity: number, isAnnual: boolean) => {
const items = [];
if (isAnnual) {
items.push({
price: BASE_ANNUAL_MONTHLY_ID,
quantity: serverQuantity,
});
return items;
}
items.push({
price: BASE_PRICE_MONTHLY_ID,
quantity: serverQuantity,
});
return items;
};

View File

@@ -63,7 +63,6 @@ export const setupDockerContainerLogsWebSocketServer = (
}
stream
.on("close", () => {
console.log("Connection closed ✅ Container Logs");
client.end();
ws.close();
})
@@ -88,7 +87,6 @@ export const setupDockerContainerLogsWebSocketServer = (
privateKey: server.sshKey?.privateKey,
});
ws.on("close", () => {
console.log("Connection closed ✅, From Container Logs WS");
client.end();
});
} else {

View File

@@ -62,9 +62,6 @@ export const setupDockerContainerTerminalWebSocketServer = (
stream
.on("close", (code: number, signal: string) => {
console.log(
`Stream :: close :: code: ${code}, signal: ${signal}`,
);
ws.send(`\nContainer closed with code: ${code}\n`);
conn.end();
})

View File

@@ -113,7 +113,6 @@ export const setupTerminalWebSocketServer = (
});
ws.on("close", () => {
console.log("Connection closed ✅");
stream.end();
});
},