feat(licenses): add migration and truncation scripts for database management

- Introduced migration script to handle database schema updates using Drizzle ORM.
- Added truncation script to clear the database schema and reset the state.
- Updated package.json scripts for easier execution of migration and truncation tasks.
- Refactored email and Stripe integration for better modularity and maintainability.
This commit is contained in:
Mauricio Siu
2025-03-19 00:57:14 -06:00
parent 523720606d
commit 473d729416
9 changed files with 91 additions and 179 deletions

View File

@@ -9,9 +9,9 @@
"typecheck": "tsc --noEmit",
"generate": "drizzle-kit generate",
"drop": "drizzle-kit drop",
"migrate": "tsx src/migrate.ts",
"truncate": "tsx src/truncate.ts",
"reset:all": "tsx src/truncate.ts && tsx src/migrate.ts",
"migrate": "tsx migrate.ts",
"truncate": "tsx truncate.ts",
"reset:all": "tsx truncate.ts && tsx migrate.ts",
"studio": "drizzle-kit studio"
},
"dependencies": {

View File

@@ -0,0 +1,11 @@
import { createTransport } from "nodemailer";
export const transporter = createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});

View File

@@ -4,9 +4,7 @@ import { cors } from "hono/cors";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
import { logger } from "./logger";
import Stripe from "stripe";
import { render } from "@react-email/render";
import { createTransport } from "nodemailer";
import { LicenseEmail } from "../templates/emails/license-email";
import { ResendLicenseEmail } from "../templates/emails/resend-license-email";
import {
@@ -18,24 +16,14 @@ import { db } from "./db";
import { eq } from "drizzle-orm";
import { licenses } from "./schema";
import "dotenv/config";
import { getLicenseFeatures, getLicenseTypeFromPriceId } from "./utils";
import { transporter } from "./email";
import type Stripe from "stripe";
import { stripe } from "./stripe";
const app = new Hono();
app.use("/*", cors());
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});
const transporter = createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
const validateSchema = z.object({
licenseKey: z.string(),
serverIp: z.string(),
@@ -45,7 +33,6 @@ const resendSchema = z.object({
licenseKey: z.string(),
});
// Endpoint para validar una licencia
app.post("/validate", zValidator("json", validateSchema), async (c) => {
const { licenseKey, serverIp } = c.req.valid("json");
@@ -58,7 +45,6 @@ app.post("/validate", zValidator("json", validateSchema), async (c) => {
}
});
// Endpoint para activar una licencia
app.post("/activate", zValidator("json", validateSchema), async (c) => {
const { licenseKey, serverIp } = c.req.valid("json");
@@ -74,7 +60,6 @@ app.post("/activate", zValidator("json", validateSchema), async (c) => {
}
});
// Endpoint para reenviar una licencia por email
app.post("/resend-license", zValidator("json", resendSchema), async (c) => {
const { licenseKey } = c.req.valid("json");
@@ -113,7 +98,6 @@ app.post("/resend-license", zValidator("json", resendSchema), async (c) => {
}
});
// Webhook de Stripe
app.post("/stripe/webhook", async (c) => {
const sig = c.req.header("stripe-signature");
const body = await c.req.json();
@@ -136,7 +120,6 @@ app.post("/stripe/webhook", async (c) => {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
// Obtener el customer
const customerResponse = await stripe.customers.retrieve(
session.customer as string,
);
@@ -145,7 +128,6 @@ app.post("/stripe/webhook", async (c) => {
throw new Error("Customer was deleted");
}
// Obtener el precio y determinar el tipo de licencia y facturación
const lineItems = await stripe.checkout.sessions.listLineItems(
session.id,
);
@@ -153,7 +135,6 @@ app.post("/stripe/webhook", async (c) => {
const { type, billingType } = getLicenseTypeFromPriceId(priceId!);
// Crear la licencia
const license = await createLicense({
customerId: customerResponse.id,
productId: session.id,
@@ -162,7 +143,6 @@ app.post("/stripe/webhook", async (c) => {
email: session.customer_details?.email!,
});
// Enviar el email con la licencia
const features = getLicenseFeatures(type);
const emailHtml = await render(
LicenseEmail({
@@ -183,7 +163,6 @@ app.post("/stripe/webhook", async (c) => {
break;
}
// Puedes agregar más casos según necesites
}
return c.json({ received: true });
@@ -196,77 +175,6 @@ app.post("/stripe/webhook", async (c) => {
}
});
// Función auxiliar para obtener las características según el tipo de licencia
function getLicenseFeatures(type: string): string[] {
const baseFeatures = [
"Unlimited deployments",
"Basic monitoring",
"Email support",
];
const premiumFeatures = [
...baseFeatures,
"Priority support",
"Advanced monitoring",
"Custom domains",
"Team collaboration",
];
const businessFeatures = [
...premiumFeatures,
"24/7 support",
"Custom integrations",
"SLA guarantees",
"Dedicated account manager",
];
switch (type) {
case "basic":
return baseFeatures;
case "premium":
return premiumFeatures;
case "business":
return businessFeatures;
default:
return baseFeatures;
}
}
// Función auxiliar para determinar el tipo de licencia según el price ID
function getLicenseTypeFromPriceId(priceId: string): {
type: "basic" | "premium" | "business";
billingType: "monthly" | "annual";
} {
const priceMap = {
[process.env.SELF_HOSTED_BASIC_PRICE_MONTHLY_ID!]: {
type: "basic",
billingType: "monthly",
},
[process.env.SELF_HOSTED_BASIC_PRICE_ANNUAL_ID!]: {
type: "basic",
billingType: "annual",
},
[process.env.SELF_HOSTED_PREMIUM_PRICE_MONTHLY_ID!]: {
type: "premium",
billingType: "monthly",
},
[process.env.SELF_HOSTED_PREMIUM_PRICE_ANNUAL_ID!]: {
type: "premium",
billingType: "annual",
},
[process.env.SELF_HOSTED_BUSINESS_PRICE_MONTHLY_ID!]: {
type: "business",
billingType: "monthly",
},
[process.env.SELF_HOSTED_BUSINESS_PRICE_ANNUAL_ID!]: {
type: "business",
billingType: "annual",
},
} as const;
return priceMap[priceId] || { type: "basic", billingType: "monthly" };
}
const port = process.env.PORT || 4000;
console.log(`Server is running on port ${port}`);

View File

@@ -0,0 +1,5 @@
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});

View File

@@ -1,82 +1,70 @@
import {
deployRemoteApplication,
deployRemoteCompose,
deployRemotePreviewApplication,
rebuildRemoteApplication,
rebuildRemoteCompose,
updateApplicationStatus,
updateCompose,
updatePreviewDeployment,
} from "@dokploy/server";
import type { DeployJob } from "./schema";
export const getLicenseFeatures = (type: string): string[] => {
const baseFeatures = [
"Unlimited deployments",
"Basic monitoring",
"Email support",
];
export const deploy = async (job: DeployJob) => {
try {
if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "running");
if (job.server) {
if (job.type === "redeploy") {
await rebuildRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
} else if (job.type === "deploy") {
await deployRemoteApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
}
}
} else if (job.applicationType === "compose") {
await updateCompose(job.composeId, {
composeStatus: "running",
});
const premiumFeatures = [
...baseFeatures,
"Priority support",
"Advanced monitoring",
"Custom domains",
"Team collaboration",
];
if (job.server) {
if (job.type === "redeploy") {
await rebuildRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
} else if (job.type === "deploy") {
await deployRemoteCompose({
composeId: job.composeId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
});
}
}
} else if (job.applicationType === "application-preview") {
await updatePreviewDeployment(job.previewDeploymentId, {
previewStatus: "running",
});
if (job.server) {
if (job.type === "deploy") {
await deployRemotePreviewApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
previewDeploymentId: job.previewDeploymentId,
});
}
}
}
} catch (_) {
if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "error");
} else if (job.applicationType === "compose") {
await updateCompose(job.composeId, {
composeStatus: "error",
});
} else if (job.applicationType === "application-preview") {
await updatePreviewDeployment(job.previewDeploymentId, {
previewStatus: "error",
});
}
const businessFeatures = [
...premiumFeatures,
"24/7 support",
"Custom integrations",
"SLA guarantees",
"Dedicated account manager",
];
switch (type) {
case "basic":
return baseFeatures;
case "premium":
return premiumFeatures;
case "business":
return businessFeatures;
default:
return baseFeatures;
}
return true;
};
export const getLicenseTypeFromPriceId = (
priceId: string,
): {
type: "basic" | "premium" | "business";
billingType: "monthly" | "annual";
} => {
const priceMap = {
[process.env.SELF_HOSTED_BASIC_PRICE_MONTHLY_ID!]: {
type: "basic",
billingType: "monthly",
},
[process.env.SELF_HOSTED_BASIC_PRICE_ANNUAL_ID!]: {
type: "basic",
billingType: "annual",
},
[process.env.SELF_HOSTED_PREMIUM_PRICE_MONTHLY_ID!]: {
type: "premium",
billingType: "monthly",
},
[process.env.SELF_HOSTED_PREMIUM_PRICE_ANNUAL_ID!]: {
type: "premium",
billingType: "annual",
},
[process.env.SELF_HOSTED_BUSINESS_PRICE_MONTHLY_ID!]: {
type: "business",
billingType: "monthly",
},
[process.env.SELF_HOSTED_BUSINESS_PRICE_ANNUAL_ID!]: {
type: "business",
billingType: "annual",
},
} as const;
return priceMap[priceId] || { type: "basic", billingType: "monthly" };
};

View File

@@ -20,7 +20,7 @@ export const createLicense = async ({
billingType: "monthly" | "annual";
email: string;
}) => {
const licenseKey = generateLicenseKey();
const licenseKey = `dokploy-${generateLicenseKey()}`;
const expiresAt = new Date();
expiresAt.setMonth(
expiresAt.getMonth() + (billingType === "annual" ? 12 : 1),

View File

@@ -16,6 +16,6 @@
"@dokploy/server/*": ["../../packages/server/src/*"]
}
},
"include": ["src/**/*"],
"include": ["src/**/*", "truncate.ts"],
"exclude": ["node_modules", "dist"]
}