From 9e3052556980d87daf7cd2e29d9d2a96e0e8f451 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 23 Mar 2025 12:15:25 -0600 Subject: [PATCH] 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. --- apps/licenses/package.json | 2 +- apps/licenses/src/db.ts | 10 ---- apps/licenses/src/email/index.ts | 7 ++- apps/licenses/src/index.ts | 77 ++++++++++++++-------------- apps/licenses/src/schema.ts | 1 - apps/licenses/src/utils/license.ts | 72 ++++++++++++++------------ apps/licenses/templates/package.json | 1 + 7 files changed, 83 insertions(+), 87 deletions(-) diff --git a/apps/licenses/package.json b/apps/licenses/package.json index 1dda40513..3ec4f59ce 100644 --- a/apps/licenses/package.json +++ b/apps/licenses/package.json @@ -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", diff --git a/apps/licenses/src/db.ts b/apps/licenses/src/db.ts index df5e02e54..d5e1d7128 100644 --- a/apps/licenses/src/db.ts +++ b/apps/licenses/src/db.ts @@ -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"; diff --git a/apps/licenses/src/email/index.ts b/apps/licenses/src/email/index.ts index 8aad6b3db..24de9adf1 100644 --- a/apps/licenses/src/email/index.ts +++ b/apps/licenses/src/email/index.ts @@ -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, }, }); diff --git a/apps/licenses/src/index.ts b/apps/licenses/src/index.ts index 3d4939bac..f9c8ebf9b 100644 --- a/apps/licenses/src/index.ts +++ b/apps/licenses/src/index.ts @@ -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, diff --git a/apps/licenses/src/schema.ts b/apps/licenses/src/schema.ts index 7cffc8cfb..0c6efcd1d 100644 --- a/apps/licenses/src/schema.ts +++ b/apps/licenses/src/schema.ts @@ -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`), diff --git a/apps/licenses/src/utils/license.ts b/apps/licenses/src/utils/license.ts index 3c4a3c299..ba8f10968 100644 --- a/apps/licenses/src/utils/license.ts +++ b/apps/licenses/src/utils/license.ts @@ -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 = ( diff --git a/apps/licenses/templates/package.json b/apps/licenses/templates/package.json index e8968afae..846b1718c 100644 --- a/apps/licenses/templates/package.json +++ b/apps/licenses/templates/package.json @@ -2,6 +2,7 @@ "name": "react-email-starter", "version": "0.1.10", "private": true, + "type": "module", "scripts": { "build": "email build", "dev": "email dev",