From feb6970b0909f9e66f6940a29133e4aeb12fcb9a Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 20 Mar 2025 01:32:13 -0600 Subject: [PATCH] feat(licenses): enhance API and database integration for checkout sessions - Updated development server port in package.json to 4002. - Introduced constants for website URLs based on environment. - Refactored database connection logic to use drizzle with PostgreSQL. - Added new API endpoint for creating checkout sessions with Stripe integration. - Implemented utility function to generate Stripe items based on license type and quantity. - Updated existing API routes to use a router for better organization. --- apps/licenses/package.json | 2 +- apps/licenses/src/constants.ts | 4 +++ apps/licenses/src/db.ts | 33 ++++++++++++++---- apps/licenses/src/index.ts | 47 +++++++++++++++++++++----- apps/licenses/src/utils/license.ts | 35 +++++++++++++++++++ apps/licenses/src/validators/stripe.ts | 7 ++++ 6 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 apps/licenses/src/constants.ts create mode 100644 apps/licenses/src/validators/stripe.ts diff --git a/apps/licenses/package.json b/apps/licenses/package.json index 2e9032855..1dda40513 100644 --- a/apps/licenses/package.json +++ b/apps/licenses/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "PORT=4000 tsx watch src/index.ts", + "dev": "PORT=4002 tsx watch src/index.ts", "build": "tsc --project tsconfig.json", "start": "node dist/index.js", "typecheck": "tsc --noEmit", diff --git a/apps/licenses/src/constants.ts b/apps/licenses/src/constants.ts new file mode 100644 index 000000000..5229f9a93 --- /dev/null +++ b/apps/licenses/src/constants.ts @@ -0,0 +1,4 @@ +export const WEBSITE_URL = + process.env.NODE_ENV === "development" + ? "http://localhost:3001" + : process.env.SITE_URL; diff --git a/apps/licenses/src/db.ts b/apps/licenses/src/db.ts index 7fd372053..df5e02e54 100644 --- a/apps/licenses/src/db.ts +++ b/apps/licenses/src/db.ts @@ -1,9 +1,30 @@ -import { drizzle } from "drizzle-orm/node-postgres"; -import { Pool } from "pg"; +// 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"; +declare global { + var db: PostgresJsDatabase | undefined; +} -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, -}); +export let db: PostgresJsDatabase; +if (process.env.NODE_ENV === "production") { + db = drizzle(postgres(process.env.DATABASE_URL!), { + schema, + }); +} else { + if (!global.db) + global.db = drizzle(postgres(process.env.DATABASE_URL!), { + schema, + }); -export const db = drizzle(pool, { schema }); + db = global.db; +} diff --git a/apps/licenses/src/index.ts b/apps/licenses/src/index.ts index 0b6cc21ec..adbfef33c 100644 --- a/apps/licenses/src/index.ts +++ b/apps/licenses/src/index.ts @@ -5,13 +5,14 @@ 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, activateLicense, deactivateLicense, + getStripeItems, } from "./utils/license"; import { db } from "./db"; import { eq, sql } from "drizzle-orm"; @@ -21,9 +22,17 @@ import { getLicenseFeatures, getLicenseTypeFromPriceId } from "./utils"; import { transporter } from "./email"; import type Stripe from "stripe"; import { stripe } from "./stripe"; +import { WEBSITE_URL } from "./constants"; +import { createCheckoutSessionSchema } from "./validators/stripe"; const app = new Hono(); -app.use("/*", cors()); +const router = new Hono(); +router.use( + "/*", + cors({ + origin: ["http://localhost:3001"], + }), +); const validateSchema = z.object({ licenseKey: z.string(), @@ -34,7 +43,7 @@ const resendSchema = z.object({ licenseKey: z.string(), }); -app.get("/health", async (c) => { +router.get("/health", async (c) => { try { await db.execute(sql`SELECT 1`); return c.json({ status: "ok" }); @@ -44,7 +53,7 @@ app.get("/health", async (c) => { } }); -app.post("/validate", zValidator("json", validateSchema), async (c) => { +router.post("/validate", zValidator("json", validateSchema), async (c) => { const { licenseKey, serverIp } = c.req.valid("json"); try { @@ -56,7 +65,7 @@ app.post("/validate", zValidator("json", validateSchema), async (c) => { } }); -app.post("/activate", zValidator("json", validateSchema), async (c) => { +router.post("/activate", zValidator("json", validateSchema), async (c) => { const { licenseKey, serverIp } = c.req.valid("json"); try { @@ -71,7 +80,26 @@ app.post("/activate", zValidator("json", validateSchema), async (c) => { } }); -app.post("/resend-license", zValidator("json", resendSchema), async (c) => { +router.post( + "/create-checkout-session", + zValidator("json", createCheckoutSessionSchema), + async (c) => { + const { type, serverQuantity, isAnnual } = c.req.valid("json"); + + const items = getStripeItems(type, serverQuantity, isAnnual); + const session = await stripe.checkout.sessions.create({ + mode: "subscription", + line_items: items, + allow_promotion_codes: true, + success_url: `${WEBSITE_URL}/license/success`, + cancel_url: `${WEBSITE_URL}#pricing`, + }); + + return c.json({ sessionId: session.id }); + }, +); + +router.post("/resend-license", zValidator("json", resendSchema), async (c) => { const { licenseKey } = c.req.valid("json"); try { @@ -107,7 +135,7 @@ app.post("/resend-license", zValidator("json", resendSchema), async (c) => { } }); -app.post("/stripe/webhook", async (c) => { +router.post("/stripe/webhook", async (c) => { const sig = c.req.header("stripe-signature"); const body = await c.req.json(); @@ -289,7 +317,8 @@ app.post("/stripe/webhook", async (c) => { } }); -const port = process.env.PORT || 4000; +app.route("/api", router); +const port = process.env.PORT || 4002; console.log(`Server is running on port ${port}`); serve({ diff --git a/apps/licenses/src/utils/license.ts b/apps/licenses/src/utils/license.ts index d9ee81404..3c4a3c299 100644 --- a/apps/licenses/src/utils/license.ts +++ b/apps/licenses/src/utils/license.ts @@ -158,3 +158,38 @@ export const getLicenseStatus = (license: License) => { return "pending payment"; } }; + +export const getStripeItems = ( + type: "basic" | "premium" | "business", + serverQuantity: number, + isAnnual: boolean, +) => { + const items = []; + + if (type === "basic") { + items.push({ + price: isAnnual + ? process.env.SELF_HOSTED_BASIC_PRICE_ANNUAL_ID + : process.env.SELF_HOSTED_BASIC_PRICE_MONTHLY_ID, + quantity: serverQuantity, + }); + } else if (type === "premium") { + items.push({ + price: isAnnual + ? process.env.SELF_HOSTED_PREMIUM_PRICE_ANNUAL_ID + : process.env.SELF_HOSTED_PREMIUM_PRICE_MONTHLY_ID, + quantity: serverQuantity, + }); + } else if (type === "business") { + items.push({ + price: isAnnual + ? process.env.SELF_HOSTED_BUSINESS_PRICE_ANNUAL_ID + : process.env.SELF_HOSTED_BUSINESS_PRICE_MONTHLY_ID, + quantity: serverQuantity, + }); + + return items; + } + + return items; +}; diff --git a/apps/licenses/src/validators/stripe.ts b/apps/licenses/src/validators/stripe.ts new file mode 100644 index 000000000..44a059f02 --- /dev/null +++ b/apps/licenses/src/validators/stripe.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const createCheckoutSessionSchema = z.object({ + type: z.enum(["basic", "premium", "business"]), + serverQuantity: z.number().min(1), + isAnnual: z.boolean(), +});