mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-12 17:35:21 +02:00
Merge branch 'canary' into feat/add-chat
This commit is contained in:
@@ -65,6 +65,9 @@ export const user = pgTable("user", {
|
||||
stripeCustomerId: text("stripeCustomerId"),
|
||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||
sendInvoiceNotifications: boolean("sendInvoiceNotifications")
|
||||
.notNull()
|
||||
.default(false),
|
||||
isEnterpriseCloud: boolean("isEnterpriseCloud").notNull().default(false),
|
||||
trustedOrigins: text("trustedOrigins").array(),
|
||||
bookmarkedTemplates: text("bookmarkedTemplates")
|
||||
|
||||
171
packages/server/src/emails/emails/invoice-notification.tsx
Normal file
171
packages/server/src/emails/emails/invoice-notification.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Row,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
export type TemplateProps = {
|
||||
userName: string;
|
||||
invoiceNumber: string;
|
||||
amountPaid: string;
|
||||
currency: string;
|
||||
date: string;
|
||||
hostedInvoiceUrl: string;
|
||||
};
|
||||
|
||||
export const InvoiceNotificationEmail = ({
|
||||
userName = "User",
|
||||
invoiceNumber = "INV-0001",
|
||||
amountPaid = "$4.50",
|
||||
currency = "usd",
|
||||
date = "2024-01-01",
|
||||
hostedInvoiceUrl = "https://invoice.stripe.com/example",
|
||||
}: TemplateProps) => {
|
||||
const previewText = `Your Dokploy invoice ${invoiceNumber} for ${amountPaid} is ready`;
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#007291",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Body className="bg-[#f4f4f5] my-auto mx-auto font-sans">
|
||||
<Container className="my-[40px] mx-auto max-w-[520px]">
|
||||
{/* Header */}
|
||||
<Section className="bg-[#09090b] rounded-t-xl px-[40px] py-[32px] text-center">
|
||||
<Img
|
||||
src="https://raw.githubusercontent.com/Dokploy/website/refs/heads/main/apps/docs/public/logo-dokploy-blackpng.png"
|
||||
width="190"
|
||||
height="120"
|
||||
alt="Dokploy"
|
||||
className="my-0 mx-auto"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Body */}
|
||||
<Section className="bg-white px-[40px] py-[32px]">
|
||||
<Heading className="text-[#09090b] text-[22px] font-semibold m-0 mb-[8px]">
|
||||
Invoice Payment Confirmed
|
||||
</Heading>
|
||||
<Text className="text-[#71717a] text-[14px] leading-[22px] m-0 mb-[24px]">
|
||||
Hello {userName}, thank you for your payment. Here's a summary
|
||||
of your invoice.
|
||||
</Text>
|
||||
|
||||
{/* Invoice Details Card */}
|
||||
<Section className="border border-solid border-[#e4e4e7] rounded-lg overflow-hidden mb-[24px]">
|
||||
<Row className="bg-[#fafafa]">
|
||||
<Column className="px-[20px] py-[14px] w-[50%]">
|
||||
<Text className="text-[#71717a] text-[12px] font-medium uppercase tracking-wider m-0">
|
||||
Invoice No.
|
||||
</Text>
|
||||
<Text className="text-[#09090b] text-[14px] font-semibold m-0 mt-[4px]">
|
||||
{invoiceNumber}
|
||||
</Text>
|
||||
</Column>
|
||||
<Column className="px-[20px] py-[14px] w-[50%]">
|
||||
<Text className="text-[#71717a] text-[12px] font-medium uppercase tracking-wider m-0">
|
||||
Date
|
||||
</Text>
|
||||
<Text className="text-[#09090b] text-[14px] font-semibold m-0 mt-[4px]">
|
||||
{date}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
<Hr className="border-[#e4e4e7] m-0" />
|
||||
<Row>
|
||||
<Column className="px-[20px] py-[14px]">
|
||||
<Text className="text-[#71717a] text-[12px] font-medium uppercase tracking-wider m-0">
|
||||
Amount Paid
|
||||
</Text>
|
||||
<Text className="text-[#09090b] text-[20px] font-bold m-0 mt-[4px]">
|
||||
{amountPaid}{" "}
|
||||
<span className="text-[#71717a] text-[12px] font-normal uppercase">
|
||||
{currency}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* Status Badge */}
|
||||
<Section className="mb-[24px]">
|
||||
<Row>
|
||||
<Column>
|
||||
<div
|
||||
className="inline-block rounded-full bg-[#dcfce7] px-[12px] py-[6px]"
|
||||
style={{ display: "inline-block" }}
|
||||
>
|
||||
<Text className="text-[#15803d] text-[12px] font-semibold m-0">
|
||||
Payment Successful
|
||||
</Text>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* CTA Button */}
|
||||
<Section className="text-center mb-[24px]">
|
||||
<Button
|
||||
href={hostedInvoiceUrl}
|
||||
className="bg-[#09090b] rounded-lg text-white text-[14px] font-semibold no-underline text-center px-[24px] py-[12px]"
|
||||
>
|
||||
View Invoice Online
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Text className="text-[#a1a1aa] text-[13px] leading-[20px] m-0 text-center">
|
||||
A PDF copy of this invoice is attached to this email for your
|
||||
records.
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
{/* Footer */}
|
||||
<Section className="bg-[#fafafa] rounded-b-xl px-[40px] py-[24px] text-center border-t border-solid border-[#e4e4e7]">
|
||||
<Text className="text-[#a1a1aa] text-[12px] leading-[18px] m-0">
|
||||
This is an automated email from{" "}
|
||||
<Link
|
||||
href="https://dokploy.com"
|
||||
className="text-[#71717a] underline"
|
||||
>
|
||||
Dokploy Cloud
|
||||
</Link>
|
||||
. If you have any questions about your billing, please contact
|
||||
our{" "}
|
||||
<Link
|
||||
href="https://discord.gg/2tBnJ3jDJc"
|
||||
className="text-[#71717a] underline"
|
||||
>
|
||||
support team
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvoiceNotificationEmail;
|
||||
175
packages/server/src/emails/emails/payment-failed.tsx
Normal file
175
packages/server/src/emails/emails/payment-failed.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Column,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Row,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
export type TemplateProps = {
|
||||
userName: string;
|
||||
invoiceNumber: string;
|
||||
amountDue: string;
|
||||
currency: string;
|
||||
date: string;
|
||||
hostedInvoiceUrl: string;
|
||||
};
|
||||
|
||||
export const PaymentFailedEmail = ({
|
||||
userName = "User",
|
||||
invoiceNumber = "INV-0001",
|
||||
amountDue = "$4.50",
|
||||
currency = "usd",
|
||||
date = "2024-01-01",
|
||||
hostedInvoiceUrl = "https://invoice.stripe.com/example",
|
||||
}: TemplateProps) => {
|
||||
const previewText = `Action required: Your Dokploy payment for ${amountDue} failed`;
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#007291",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Body className="bg-[#f4f4f5] my-auto mx-auto font-sans">
|
||||
<Container className="my-[40px] mx-auto max-w-[520px]">
|
||||
{/* Header */}
|
||||
<Section className="bg-[#09090b] rounded-t-xl px-[40px] py-[32px] text-center">
|
||||
<Img
|
||||
src="https://raw.githubusercontent.com/Dokploy/website/refs/heads/main/apps/docs/public/logo-dokploy-blackpng.png"
|
||||
width="190"
|
||||
height="120"
|
||||
alt="Dokploy"
|
||||
className="my-0 mx-auto"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Body */}
|
||||
<Section className="bg-white px-[40px] py-[32px]">
|
||||
<Heading className="text-[#09090b] text-[22px] font-semibold m-0 mb-[8px]">
|
||||
Payment Failed
|
||||
</Heading>
|
||||
<Text className="text-[#71717a] text-[14px] leading-[22px] m-0 mb-[24px]">
|
||||
Hello {userName}, we were unable to process your payment. Please
|
||||
update your payment method to avoid service interruption.
|
||||
</Text>
|
||||
|
||||
{/* Invoice Details Card */}
|
||||
<Section className="border border-solid border-[#e4e4e7] rounded-lg overflow-hidden mb-[24px]">
|
||||
<Row className="bg-[#fafafa]">
|
||||
<Column className="px-[20px] py-[14px] w-[50%]">
|
||||
<Text className="text-[#71717a] text-[12px] font-medium uppercase tracking-wider m-0">
|
||||
Invoice No.
|
||||
</Text>
|
||||
<Text className="text-[#09090b] text-[14px] font-semibold m-0 mt-[4px]">
|
||||
{invoiceNumber}
|
||||
</Text>
|
||||
</Column>
|
||||
<Column className="px-[20px] py-[14px] w-[50%]">
|
||||
<Text className="text-[#71717a] text-[12px] font-medium uppercase tracking-wider m-0">
|
||||
Date
|
||||
</Text>
|
||||
<Text className="text-[#09090b] text-[14px] font-semibold m-0 mt-[4px]">
|
||||
{date}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
<Hr className="border-[#e4e4e7] m-0" />
|
||||
<Row>
|
||||
<Column className="px-[20px] py-[14px]">
|
||||
<Text className="text-[#71717a] text-[12px] font-medium uppercase tracking-wider m-0">
|
||||
Amount Due
|
||||
</Text>
|
||||
<Text className="text-[#09090b] text-[20px] font-bold m-0 mt-[4px]">
|
||||
{amountDue}{" "}
|
||||
<span className="text-[#71717a] text-[12px] font-normal uppercase">
|
||||
{currency}
|
||||
</span>
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* Status Badge */}
|
||||
<Section className="mb-[24px]">
|
||||
<Row>
|
||||
<Column>
|
||||
<div
|
||||
className="inline-block rounded-full bg-[#fee2e2] px-[12px] py-[6px]"
|
||||
style={{ display: "inline-block" }}
|
||||
>
|
||||
<Text className="text-[#dc2626] text-[12px] font-semibold m-0">
|
||||
Payment Failed
|
||||
</Text>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* Warning */}
|
||||
<Section className="bg-[#fefce8] border border-solid border-[#fef08a] rounded-lg px-[20px] py-[16px] mb-[24px]">
|
||||
<Text className="text-[#854d0e] text-[13px] leading-[20px] m-0">
|
||||
If the payment issue is not resolved, your servers will be
|
||||
deactivated. Please update your payment method as soon as
|
||||
possible.
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
{/* CTA Button */}
|
||||
<Section className="text-center mb-[24px]">
|
||||
<Button
|
||||
href={hostedInvoiceUrl}
|
||||
className="bg-[#dc2626] rounded-lg text-white text-[14px] font-semibold no-underline text-center px-[24px] py-[12px]"
|
||||
>
|
||||
Update Payment Method
|
||||
</Button>
|
||||
</Section>
|
||||
</Section>
|
||||
|
||||
{/* Footer */}
|
||||
<Section className="bg-[#fafafa] rounded-b-xl px-[40px] py-[24px] text-center border-t border-solid border-[#e4e4e7]">
|
||||
<Text className="text-[#a1a1aa] text-[12px] leading-[18px] m-0">
|
||||
This is an automated email from{" "}
|
||||
<Link
|
||||
href="https://dokploy.com"
|
||||
className="text-[#71717a] underline"
|
||||
>
|
||||
Dokploy Cloud
|
||||
</Link>
|
||||
. If you have any questions about your billing, please contact
|
||||
our{" "}
|
||||
<Link
|
||||
href="https://discord.gg/2tBnJ3jDJc"
|
||||
className="text-[#71717a] underline"
|
||||
>
|
||||
support team
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentFailedEmail;
|
||||
@@ -440,17 +440,16 @@ export const removeCompose = async (
|
||||
}
|
||||
} else {
|
||||
const command = `
|
||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||
cd ${projectPath} && env -i PATH="$PATH" docker compose -p ${compose.appName} down ${
|
||||
docker network disconnect ${compose.appName} dokploy-traefik;
|
||||
env -i PATH="$PATH" docker compose -p ${compose.appName} down ${
|
||||
deleteVolumes ? "--volumes" : ""
|
||||
} && rm -rf ${projectPath}`;
|
||||
};
|
||||
rm -rf ${projectPath}`;
|
||||
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await execAsync(command, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
await execAsync(command);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -355,14 +355,45 @@ export const getContainersByAppLabel = async (
|
||||
};
|
||||
|
||||
export const getContainerLogs = async (
|
||||
appName: string,
|
||||
appNameOrId: string,
|
||||
tail = 100,
|
||||
since = "all",
|
||||
search?: string,
|
||||
serverId?: string | null,
|
||||
useContainerIdDirectly = false,
|
||||
): Promise<string> => {
|
||||
const exec = (cmd: string) =>
|
||||
serverId ? execAsyncRemote(serverId, cmd) : execAsync(cmd);
|
||||
|
||||
let target = appNameOrId;
|
||||
let isService = false;
|
||||
|
||||
if (!useContainerIdDirectly) {
|
||||
// Find the real container ID by appName filter
|
||||
const findResult = await exec(
|
||||
`docker ps -q --filter "name=^${appNameOrId}" | head -1`,
|
||||
);
|
||||
const containerId = findResult.stdout.trim();
|
||||
|
||||
if (!containerId) {
|
||||
// Fallback: try as a swarm service
|
||||
const svcResult = await exec(
|
||||
`docker service ls -q --filter "name=${appNameOrId}" | head -1`,
|
||||
);
|
||||
const serviceId = svcResult.stdout.trim();
|
||||
if (!serviceId) {
|
||||
throw new Error(`No container or service found for: ${appNameOrId}`);
|
||||
}
|
||||
isService = true;
|
||||
} else {
|
||||
target = containerId;
|
||||
}
|
||||
}
|
||||
|
||||
const sinceFlag = since === "all" ? "" : `--since ${since}`;
|
||||
const baseCommand = `docker container logs --timestamps --tail ${tail} ${sinceFlag} ${appName}`;
|
||||
const baseCommand = isService
|
||||
? `docker service logs --timestamps --raw --tail ${tail} ${sinceFlag} ${target}`
|
||||
: `docker container logs --timestamps --tail ${tail} ${sinceFlag} ${target}`;
|
||||
|
||||
const escapedSearch = search?.replace(/'/g, "'\\''") ?? "";
|
||||
const command = search
|
||||
@@ -370,10 +401,7 @@ export const getContainerLogs = async (
|
||||
: `${baseCommand} 2>&1`;
|
||||
|
||||
try {
|
||||
const result = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
const result = await exec(command);
|
||||
return result.stdout;
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
|
||||
@@ -106,6 +106,7 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
|
||||
let envContent = `APP_NAME=${appName}\n`;
|
||||
envContent += `COMPOSE_PROJECT_NAME=${appName}\n`;
|
||||
envContent += env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker";
|
||||
|
||||
@@ -19,6 +19,7 @@ export const sendEmailNotification = async (
|
||||
connection: typeof email.$inferInsert,
|
||||
subject: string,
|
||||
htmlContent: string,
|
||||
attachments?: { filename: string; content: Buffer }[],
|
||||
) => {
|
||||
try {
|
||||
const {
|
||||
@@ -41,6 +42,7 @@ export const sendEmailNotification = async (
|
||||
subject,
|
||||
html: htmlContent,
|
||||
textEncoding: "base64",
|
||||
attachments,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
@@ -151,16 +151,18 @@ export const createRouterConfig = async (
|
||||
routerConfig.middlewares?.push("redirect-to-https");
|
||||
} else {
|
||||
// Add path rewriting middleware if needed
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const pathMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(pathMiddleware);
|
||||
}
|
||||
|
||||
// stripPrefix must come before addPrefix so Traefik strips the
|
||||
// public path first, then prepends the internal path.
|
||||
if (stripPath && path && path !== "/") {
|
||||
const stripMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(stripMiddleware);
|
||||
}
|
||||
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const pathMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(pathMiddleware);
|
||||
}
|
||||
|
||||
// redirects - skip for preview deployments as wildcard subdomains
|
||||
// should not inherit parent redirect rules (e.g., www-redirect)
|
||||
if (domain.domainType !== "preview") {
|
||||
|
||||
@@ -3,10 +3,12 @@ export const sendEmail = async ({
|
||||
email,
|
||||
subject,
|
||||
text,
|
||||
attachments,
|
||||
}: {
|
||||
email: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
attachments?: { filename: string; content: Buffer }[];
|
||||
}) => {
|
||||
await sendEmailNotification(
|
||||
{
|
||||
@@ -19,6 +21,7 @@ export const sendEmail = async ({
|
||||
},
|
||||
subject,
|
||||
text,
|
||||
attachments,
|
||||
);
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user