mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 21:55:24 +02:00
feat(licenses): improve development setup and refactor email handling
- Updated development script in package.json to load environment variables from dotenv. - Refactored email imports to use named exports for better clarity. - Removed unused database expiration logic and adjusted license validation to rely on Stripe subscription status. - Enhanced error logging in webhook processing and improved console output for server status. - Updated SMTP configuration to use more descriptive environment variable names. - Removed the expiresAt field from the database schema to streamline license management.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "PORT=4002 tsx watch src/index.ts",
|
||||
"dev": "PORT=4002 tsx watch -r dotenv/config src/index.ts",
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
// import { drizzle } from "drizzle-orm/node-postgres";
|
||||
// import { Pool } from "pg";
|
||||
// import * as schema from "./schema";
|
||||
|
||||
// const pool = new Pool({
|
||||
// connectionString: process.env.DATABASE_URL,
|
||||
// });
|
||||
|
||||
// export const db = drizzle(pool, { schema });
|
||||
|
||||
import { type PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "./schema";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { createTransport } from "nodemailer";
|
||||
|
||||
export const transporter = createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
host: process.env.SMTP_SERVER,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
user: process.env.SMTP_USERNAME,
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ import { z } from "zod";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { logger } from "./logger";
|
||||
import { render } from "@react-email/render";
|
||||
import LicenseEmail from "../templates/emails/license-email";
|
||||
import ResendLicenseEmail from "../templates/emails/resend-license-email";
|
||||
import { LicenseEmail } from "../templates/emails/license-email";
|
||||
import { ResendLicenseEmail } from "../templates/emails/resend-license-email";
|
||||
import {
|
||||
createLicense,
|
||||
validateLicense,
|
||||
@@ -34,6 +34,8 @@ router.use(
|
||||
}),
|
||||
);
|
||||
|
||||
console.log(process.env.DATABASE_URL);
|
||||
|
||||
const validateSchema = z.object({
|
||||
licenseKey: z.string(),
|
||||
serverIp: z.string(),
|
||||
@@ -117,14 +119,20 @@ router.post("/resend-license", zValidator("json", resendSchema), async (c) => {
|
||||
ResendLicenseEmail({
|
||||
licenseKey: license.licenseKey,
|
||||
productName: `Dokploy Self Hosted ${license.type}`,
|
||||
expirationDate: new Date(license.expiresAt),
|
||||
// TODO: Add expiration date
|
||||
expirationDate: new Date(),
|
||||
requestDate: new Date(),
|
||||
customerName: license.email,
|
||||
}),
|
||||
);
|
||||
|
||||
// await transporter.sendMail({
|
||||
// from: fromAddress,
|
||||
// to: toAddresses.join(", "),
|
||||
// subject,
|
||||
// html: htmlContent,
|
||||
// });
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM,
|
||||
from: process.env.SMTP_FROM_ADDRESS,
|
||||
to: license.email,
|
||||
subject: "Your Dokploy License Key",
|
||||
html: emailHtml,
|
||||
@@ -136,16 +144,15 @@ router.post("/resend-license", zValidator("json", resendSchema), async (c) => {
|
||||
return c.json({ success: false, error: "Error resending license" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/stripe/webhook", async (c) => {
|
||||
const rawBody = await c.req.raw.text();
|
||||
const sig = c.req.header("stripe-signature");
|
||||
const body = await c.req.json();
|
||||
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
JSON.stringify(body),
|
||||
rawBody,
|
||||
sig!,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!,
|
||||
);
|
||||
@@ -154,6 +161,18 @@ router.post("/stripe/webhook", async (c) => {
|
||||
return c.json({ error: "Webhook signature verification failed" }, 400);
|
||||
}
|
||||
|
||||
const allowedEvents = [
|
||||
"checkout.session.completed",
|
||||
"customer.subscription.updated",
|
||||
"invoice.payment_succeeded",
|
||||
"invoice.payment_failed",
|
||||
"customer.subscription.deleted",
|
||||
];
|
||||
|
||||
if (!allowedEvents.includes(event.type)) {
|
||||
return c.json({ error: "Event not allowed" }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed": {
|
||||
@@ -183,19 +202,23 @@ router.post("/stripe/webhook", async (c) => {
|
||||
stripeSubscriptionId: session.id,
|
||||
});
|
||||
|
||||
console.log("License created", license);
|
||||
|
||||
const features = getLicenseFeatures(type);
|
||||
console.log("Features", features);
|
||||
const emailHtml = await render(
|
||||
LicenseEmail({
|
||||
customerName: customerResponse.name || "Customer",
|
||||
licenseKey: license.licenseKey,
|
||||
productName: `Dokploy Self Hosted ${type}`,
|
||||
expirationDate: new Date(license.expiresAt),
|
||||
// TODO: Add expiration date
|
||||
expirationDate: new Date(),
|
||||
features: features,
|
||||
}),
|
||||
);
|
||||
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM,
|
||||
from: process.env.SMTP_FROM_ADDRESS,
|
||||
to: license.email,
|
||||
subject: "Your Dokploy License Key",
|
||||
html: emailHtml,
|
||||
@@ -230,7 +253,10 @@ router.post("/stripe/webhook", async (c) => {
|
||||
const customerResponse = await stripe.customers.retrieve(
|
||||
invoice.customer as string,
|
||||
);
|
||||
if (suscription.status !== "active" || customerResponse.deleted) break;
|
||||
if (suscription.status !== "active" || customerResponse.deleted) {
|
||||
await deactivateLicense(invoice.subscription as string);
|
||||
break;
|
||||
}
|
||||
|
||||
const existingLicense = await db.query.licenses.findFirst({
|
||||
where: eq(licenses.stripeCustomerId, invoice.customer as string),
|
||||
@@ -238,38 +264,13 @@ router.post("/stripe/webhook", async (c) => {
|
||||
|
||||
if (!existingLicense) break;
|
||||
|
||||
const newExpirationDate = new Date();
|
||||
newExpirationDate.setMonth(
|
||||
newExpirationDate.getMonth() +
|
||||
(existingLicense.billingType === "annual" ? 12 : 1),
|
||||
);
|
||||
|
||||
await db
|
||||
.update(licenses)
|
||||
.set({
|
||||
expiresAt: newExpirationDate,
|
||||
status: "active",
|
||||
})
|
||||
.where(eq(licenses.id, existingLicense.id));
|
||||
|
||||
const features = getLicenseFeatures(existingLicense.type);
|
||||
const emailHtml = await render(
|
||||
LicenseEmail({
|
||||
customerName: customerResponse.name || "Customer",
|
||||
licenseKey: existingLicense.licenseKey,
|
||||
productName: `Dokploy Self Hosted ${existingLicense.type}`,
|
||||
expirationDate: new Date(newExpirationDate),
|
||||
features: features,
|
||||
}),
|
||||
);
|
||||
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM,
|
||||
to: existingLicense.email,
|
||||
subject: "Your Dokploy License Has Been Renewed",
|
||||
html: emailHtml,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -311,7 +312,7 @@ router.post("/stripe/webhook", async (c) => {
|
||||
|
||||
return c.json({ received: true });
|
||||
} catch (error) {
|
||||
logger.error("Error processing webhook:", error);
|
||||
console.error("Error processing webhook:", error);
|
||||
if (error instanceof Error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
@@ -321,7 +322,7 @@ router.post("/stripe/webhook", async (c) => {
|
||||
|
||||
app.route("/api", router);
|
||||
const port = process.env.PORT || 4002;
|
||||
console.log(`Server is running on port ${port}`);
|
||||
console.log(`Server is running on port http://localhost:${port}`);
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
|
||||
@@ -26,7 +26,6 @@ export const licenses = pgTable("licenses", {
|
||||
serverIp: text("server_ip"),
|
||||
activatedAt: timestamp("activated_at"),
|
||||
lastVerifiedAt: timestamp("last_verified_at"),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
stripeCustomerId: text("stripeCustomerId").notNull(),
|
||||
stripeSubscriptionId: text("stripeSubscriptionId").notNull(),
|
||||
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { db } from "../db";
|
||||
import { type License, licenses } from "../schema";
|
||||
import { licenses } from "../schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { stripe } from "../stripe";
|
||||
import type Stripe from "stripe";
|
||||
|
||||
export const generateLicenseKey = () => {
|
||||
return randomBytes(32).toString("hex");
|
||||
@@ -25,10 +27,6 @@ export const createLicense = async ({
|
||||
stripeSubscriptionId,
|
||||
}: CreateLicenseProps) => {
|
||||
const licenseKey = `dokploy-${generateLicenseKey()}`;
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setMonth(
|
||||
expiresAt.getMonth() + (billingType === "annual" ? 12 : 1),
|
||||
);
|
||||
|
||||
const license = await db
|
||||
.insert(licenses)
|
||||
@@ -37,7 +35,6 @@ export const createLicense = async ({
|
||||
licenseKey,
|
||||
type,
|
||||
billingType,
|
||||
expiresAt,
|
||||
email,
|
||||
stripeCustomerId,
|
||||
stripeSubscriptionId,
|
||||
@@ -59,26 +56,21 @@ export const validateLicense = async (
|
||||
return { isValid: false, error: "License not found" };
|
||||
}
|
||||
|
||||
if (license.status !== "active") {
|
||||
const suscription = await stripe.subscriptions.retrieve(
|
||||
license.stripeSubscriptionId,
|
||||
);
|
||||
|
||||
if (suscription.status !== "active") {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `License is ${getLicenseStatus(license)}`,
|
||||
error: `License is ${getLicenseStatus(suscription)}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (new Date() > license.expiresAt) {
|
||||
await db
|
||||
.update(licenses)
|
||||
.set({ status: "expired" })
|
||||
.where(eq(licenses.id, license.id));
|
||||
return { isValid: false, error: "License has expired" };
|
||||
}
|
||||
|
||||
if (license.serverIp && serverIp && license.serverIp !== serverIp) {
|
||||
return { isValid: false, error: "Invalid server IP" };
|
||||
}
|
||||
|
||||
// Update last verified timestamp
|
||||
await db
|
||||
.update(licenses)
|
||||
.set({ lastVerifiedAt: new Date() })
|
||||
@@ -96,16 +88,12 @@ export const activateLicense = async (licenseKey: string, serverIp: string) => {
|
||||
throw new Error("License not found");
|
||||
}
|
||||
|
||||
if (license.status !== "active") {
|
||||
throw new Error("License is not active");
|
||||
}
|
||||
const suscription = await stripe.subscriptions.retrieve(
|
||||
license.stripeSubscriptionId,
|
||||
);
|
||||
|
||||
if (new Date() > license.expiresAt) {
|
||||
await db
|
||||
.update(licenses)
|
||||
.set({ status: "expired" })
|
||||
.where(eq(licenses.id, license.id));
|
||||
throw new Error("License has expired");
|
||||
if (suscription.status !== "active") {
|
||||
throw new Error(`License is ${getLicenseStatus(suscription)}`);
|
||||
}
|
||||
|
||||
if (license.serverIp && license.serverIp !== serverIp) {
|
||||
@@ -141,22 +129,40 @@ export const deactivateLicense = async (stripeSubscriptionId: string) => {
|
||||
.where(eq(licenses.id, license.id));
|
||||
};
|
||||
|
||||
export const getLicenseStatus = (license: License) => {
|
||||
export const getLicenseStatus = async (license: Stripe.Subscription) => {
|
||||
if (license.status === "active") {
|
||||
return "active";
|
||||
}
|
||||
|
||||
if (license.status === "expired") {
|
||||
return "expired";
|
||||
if (license.status === "canceled") {
|
||||
return "canceled";
|
||||
}
|
||||
|
||||
if (license.status === "cancelled") {
|
||||
return "cancelled";
|
||||
if (license.status === "incomplete") {
|
||||
return "incomplete";
|
||||
}
|
||||
|
||||
if (license.status === "payment_pending") {
|
||||
return "pending payment";
|
||||
if (license.status === "incomplete_expired") {
|
||||
return "incomplete expired";
|
||||
}
|
||||
|
||||
if (license.status === "past_due") {
|
||||
return "past due";
|
||||
}
|
||||
|
||||
if (license.status === "paused") {
|
||||
return "paused";
|
||||
}
|
||||
|
||||
if (license.status === "trialing") {
|
||||
return "trialing";
|
||||
}
|
||||
|
||||
if (license.status === "unpaid") {
|
||||
return "unpaid";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
export const getStripeItems = (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "react-email-starter",
|
||||
"version": "0.1.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "email build",
|
||||
"dev": "email dev",
|
||||
|
||||
Reference in New Issue
Block a user