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.
This commit is contained in:
Mauricio Siu
2025-03-20 01:32:13 -06:00
parent 78682fa359
commit feb6970b09
6 changed files with 112 additions and 16 deletions

View File

@@ -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",

View File

@@ -0,0 +1,4 @@
export const WEBSITE_URL =
process.env.NODE_ENV === "development"
? "http://localhost:3001"
: process.env.SITE_URL;

View File

@@ -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<typeof schema> | undefined;
}
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export let db: PostgresJsDatabase<typeof schema>;
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;
}

View File

@@ -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({

View File

@@ -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;
};

View File

@@ -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(),
});