mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(licenses): implement license management system with email notifications
- Added database schema for licenses, including types and statuses. - Implemented license creation, validation, and activation functionalities. - Integrated email templates for sending license keys and resend requests. - Updated package dependencies and configuration for PostgreSQL integration. - Introduced migration and truncation scripts for database management. - Enhanced API endpoints for license operations with error handling and validation.
This commit is contained in:
15
apps/licenses/drizzle.config.ts
Normal file
15
apps/licenses/drizzle.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Config } from "drizzle-kit";
|
||||
import "dotenv/config";
|
||||
|
||||
const connectionString = process.env.DATABASE_URL!;
|
||||
|
||||
export default {
|
||||
schema: "./src/schema.ts",
|
||||
out: "./drizzle",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: connectionString,
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
} satisfies Config;
|
||||
21
apps/licenses/drizzle/0000_furry_nico_minoru.sql
Normal file
21
apps/licenses/drizzle/0000_furry_nico_minoru.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
CREATE TYPE "public"."billing_type" AS ENUM('monthly', 'annual');--> statement-breakpoint
|
||||
CREATE TYPE "public"."license_status" AS ENUM('active', 'expired', 'cancelled');--> statement-breakpoint
|
||||
CREATE TYPE "public"."license_type" AS ENUM('basic', 'premium', 'business');--> statement-breakpoint
|
||||
CREATE TABLE "licenses" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"customer_id" text NOT NULL,
|
||||
"product_id" text NOT NULL,
|
||||
"license_key" text NOT NULL,
|
||||
"status" "license_status" DEFAULT 'active' NOT NULL,
|
||||
"type" "license_type" NOT NULL,
|
||||
"billing_type" "billing_type" NOT NULL,
|
||||
"server_ip" text,
|
||||
"activated_at" timestamp,
|
||||
"last_verified_at" timestamp,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
"metadata" text,
|
||||
"email" text NOT NULL,
|
||||
CONSTRAINT "licenses_license_key_unique" UNIQUE("license_key")
|
||||
);
|
||||
164
apps/licenses/drizzle/meta/0000_snapshot.json
Normal file
164
apps/licenses/drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"id": "41745f43-6627-49f6-afa3-ab192559b5a7",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.licenses": {
|
||||
"name": "licenses",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"product_id": {
|
||||
"name": "product_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"license_key": {
|
||||
"name": "license_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "license_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'active'"
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "license_type",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"billing_type": {
|
||||
"name": "billing_type",
|
||||
"type": "billing_type",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"server_ip": {
|
||||
"name": "server_ip",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"activated_at": {
|
||||
"name": "activated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_verified_at": {
|
||||
"name": "last_verified_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"licenses_license_key_unique": {
|
||||
"name": "licenses_license_key_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"license_key"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.billing_type": {
|
||||
"name": "billing_type",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"monthly",
|
||||
"annual"
|
||||
]
|
||||
},
|
||||
"public.license_status": {
|
||||
"name": "license_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"active",
|
||||
"expired",
|
||||
"cancelled"
|
||||
]
|
||||
},
|
||||
"public.license_type": {
|
||||
"name": "license_type",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"basic",
|
||||
"premium",
|
||||
"business"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
apps/licenses/drizzle/meta/_journal.json
Normal file
13
apps/licenses/drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1742364501431,
|
||||
"tag": "0000_furry_nico_minoru",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,27 +6,41 @@
|
||||
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"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",
|
||||
"studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "^0.0.21",
|
||||
"@hono/node-server": "^1.12.1",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"@react-email/render": "^1.0.5",
|
||||
"@types/pg": "^8.11.11",
|
||||
"dotenv": "^16.3.1",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"hono": "^4.5.8",
|
||||
"nodemailer": "6.9.14",
|
||||
"pg": "^8.14.1",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"zod": "^3.23.4",
|
||||
"postgres": "3.4.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@hono/node-server": "^1.12.1",
|
||||
"hono": "^4.5.8",
|
||||
"dotenv": "^16.3.1",
|
||||
"stripe": "17.2.0"
|
||||
"stripe": "17.2.0",
|
||||
"zod": "^3.23.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.2",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1"
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0"
|
||||
}
|
||||
|
||||
9
apps/licenses/src/db.ts
Normal file
9
apps/licenses/src/db.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
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 });
|
||||
@@ -1,39 +1,276 @@
|
||||
import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
import "dotenv/config";
|
||||
import { cors } from "hono/cors";
|
||||
import { z } from "zod";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { logger } from "./logger.js";
|
||||
import { deployJobSchema } from "./schema.js";
|
||||
import { logger } from "./logger";
|
||||
import Stripe from "stripe";
|
||||
const app = new Hono();
|
||||
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 {
|
||||
createLicense,
|
||||
validateLicense,
|
||||
activateLicense,
|
||||
} from "./utils/license";
|
||||
import { db } from "./db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { licenses } from "./schema";
|
||||
import "dotenv/config";
|
||||
|
||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||
const data = c.req.valid("json");
|
||||
return c.json(
|
||||
{
|
||||
message: "Deployment Added",
|
||||
},
|
||||
200,
|
||||
);
|
||||
const app = new Hono();
|
||||
app.use("/*", cors());
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
});
|
||||
|
||||
// Stripe webhook
|
||||
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(),
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
try {
|
||||
const result = await validateLicense(licenseKey, serverIp);
|
||||
return c.json(result);
|
||||
} catch (error) {
|
||||
logger.error("Error validating license:", error);
|
||||
return c.json({ isValid: false, error: "Error validating license" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint para activar una licencia
|
||||
app.post("/activate", zValidator("json", validateSchema), async (c) => {
|
||||
const { licenseKey, serverIp } = c.req.valid("json");
|
||||
|
||||
try {
|
||||
const license = await activateLicense(licenseKey, serverIp);
|
||||
return c.json({ success: true, license });
|
||||
} catch (error) {
|
||||
logger.error("Error activating license:", error);
|
||||
if (error instanceof Error) {
|
||||
return c.json({ success: false, error: error.message }, 400);
|
||||
}
|
||||
return c.json({ success: false, error: "Unknown error occurred" }, 400);
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint para reenviar una licencia por email
|
||||
app.post("/resend-license", zValidator("json", resendSchema), async (c) => {
|
||||
const { licenseKey } = c.req.valid("json");
|
||||
|
||||
try {
|
||||
const license = await db.query.licenses.findFirst({
|
||||
where: eq(licenses.licenseKey, licenseKey),
|
||||
});
|
||||
|
||||
if (!license) {
|
||||
return c.json({ success: false, error: "License not found" }, 404);
|
||||
}
|
||||
|
||||
// Generar el email
|
||||
const emailHtml = await render(
|
||||
ResendLicenseEmail({
|
||||
customerName: license.customerId,
|
||||
licenseKey: license.licenseKey,
|
||||
productName: `Dokploy Self Hosted ${license.type}`,
|
||||
expirationDate: new Date(license.expiresAt),
|
||||
requestDate: new Date(),
|
||||
}),
|
||||
);
|
||||
|
||||
// Enviar el email
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM,
|
||||
to: license.email,
|
||||
subject: "Your Dokploy License Key",
|
||||
html: emailHtml,
|
||||
});
|
||||
|
||||
return c.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error("Error resending license:", error);
|
||||
return c.json({ success: false, error: "Error resending license" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Webhook de Stripe
|
||||
app.post("/stripe/webhook", async (c) => {
|
||||
const sig = c.req.header("stripe-signature");
|
||||
const body = await c.req.json();
|
||||
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
body,
|
||||
c.req.header("stripe-signature"),
|
||||
process.env.STRIPE_WEBHOOK_SECRET,
|
||||
);
|
||||
let event: Stripe.Event;
|
||||
|
||||
return c.json({ status: "ok" });
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
JSON.stringify(body),
|
||||
sig!,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error("Webhook signature verification failed:", err);
|
||||
return c.json({ error: "Webhook signature verification failed" }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
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,
|
||||
);
|
||||
|
||||
if (customerResponse.deleted) {
|
||||
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,
|
||||
);
|
||||
const priceId = lineItems.data[0].price?.id;
|
||||
|
||||
const { type, billingType } = getLicenseTypeFromPriceId(priceId!);
|
||||
|
||||
// Crear la licencia
|
||||
const license = await createLicense({
|
||||
customerId: customerResponse.id,
|
||||
productId: session.id,
|
||||
type,
|
||||
billingType,
|
||||
email: session.customer_details?.email!,
|
||||
});
|
||||
|
||||
// Enviar el email con la licencia
|
||||
const features = getLicenseFeatures(type);
|
||||
const emailHtml = await render(
|
||||
LicenseEmail({
|
||||
customerName: customerResponse.name || "Customer",
|
||||
licenseKey: license.licenseKey,
|
||||
productName: `Dokploy Self Hosted ${type}`,
|
||||
expirationDate: new Date(license.expiresAt),
|
||||
features: features,
|
||||
}),
|
||||
);
|
||||
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_FROM,
|
||||
to: license.email,
|
||||
subject: "Your Dokploy License Key",
|
||||
html: emailHtml,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
// Puedes agregar más casos según necesites
|
||||
}
|
||||
|
||||
return c.json({ received: true });
|
||||
} catch (error) {
|
||||
logger.error("Error processing webhook:", error);
|
||||
if (error instanceof Error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
return c.json({ error: "Unknown error occurred" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/health", async (c) => {
|
||||
return c.json({ status: "ok" });
|
||||
});
|
||||
// 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 port = Number.parseInt(process.env.PORT || "3000");
|
||||
logger.info("Starting Deployments Server ✅", port);
|
||||
serve({ fetch: app.fetch, port });
|
||||
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}`);
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port: Number(port),
|
||||
});
|
||||
|
||||
22
apps/licenses/src/migrate.ts
Normal file
22
apps/licenses/src/migrate.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import postgres from "postgres";
|
||||
import "dotenv/config";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const connectionString = process.env.DATABASE_URL!;
|
||||
const sql = postgres(connectionString, { max: 1 });
|
||||
const db = drizzle(sql, { schema });
|
||||
|
||||
await migrate(db, { migrationsFolder: "drizzle" })
|
||||
.then(() => {
|
||||
console.log("Migration complete");
|
||||
sql.end();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Migration failed", error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => {
|
||||
sql.end();
|
||||
});
|
||||
@@ -1,34 +1,37 @@
|
||||
import { z } from "zod";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { pgEnum, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
|
||||
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application"),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("compose"),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
previewDeploymentId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy"]),
|
||||
applicationType: z.literal("application-preview"),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
export const licenseStatusEnum = pgEnum("license_status", [
|
||||
"active",
|
||||
"expired",
|
||||
"cancelled",
|
||||
]);
|
||||
|
||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||
export const licenseTypeEnum = pgEnum("license_type", [
|
||||
"basic",
|
||||
"premium",
|
||||
"business",
|
||||
]);
|
||||
|
||||
export const billingTypeEnum = pgEnum("billing_type", ["monthly", "annual"]);
|
||||
|
||||
export const licenses = pgTable("licenses", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
customerId: text("customer_id").notNull(),
|
||||
productId: text("product_id").notNull(),
|
||||
licenseKey: text("license_key").notNull().unique(),
|
||||
status: licenseStatusEnum("status").notNull().default("active"),
|
||||
type: licenseTypeEnum("type").notNull(),
|
||||
billingType: billingTypeEnum("billing_type").notNull(),
|
||||
serverIp: text("server_ip"),
|
||||
activatedAt: timestamp("activated_at"),
|
||||
lastVerifiedAt: timestamp("last_verified_at"),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`),
|
||||
metadata: text("metadata"),
|
||||
email: text("email").notNull(),
|
||||
});
|
||||
|
||||
export type License = typeof licenses.$inferSelect;
|
||||
export type NewLicense = typeof licenses.$inferInsert;
|
||||
|
||||
24
apps/licenses/src/truncate.ts
Normal file
24
apps/licenses/src/truncate.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
// Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import "dotenv/config";
|
||||
import postgres from "postgres";
|
||||
|
||||
const connectionString = process.env.DATABASE_URL!;
|
||||
|
||||
const pg = postgres(connectionString, { max: 1 });
|
||||
const db = drizzle(pg);
|
||||
|
||||
const clearDb = async (): Promise<void> => {
|
||||
try {
|
||||
const tablesQuery = sql<string>`DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP schema drizzle CASCADE;`;
|
||||
const tables = await db.execute(tablesQuery);
|
||||
console.log(tables);
|
||||
await pg.end();
|
||||
} catch (error) {
|
||||
console.error("Error cleaning database", error);
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
clearDb();
|
||||
119
apps/licenses/src/utils/license.ts
Normal file
119
apps/licenses/src/utils/license.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { db } from "../db";
|
||||
import { licenses } from "../schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const generateLicenseKey = () => {
|
||||
return randomBytes(32).toString("hex");
|
||||
};
|
||||
|
||||
export const createLicense = async ({
|
||||
customerId,
|
||||
productId,
|
||||
type,
|
||||
billingType,
|
||||
email,
|
||||
}: {
|
||||
customerId: string;
|
||||
productId: string;
|
||||
type: "basic" | "premium" | "business";
|
||||
billingType: "monthly" | "annual";
|
||||
email: string;
|
||||
}) => {
|
||||
const licenseKey = generateLicenseKey();
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setMonth(
|
||||
expiresAt.getMonth() + (billingType === "annual" ? 12 : 1),
|
||||
);
|
||||
|
||||
const license = await db
|
||||
.insert(licenses)
|
||||
.values({
|
||||
customerId,
|
||||
productId,
|
||||
licenseKey,
|
||||
type,
|
||||
billingType,
|
||||
expiresAt,
|
||||
email,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return license[0];
|
||||
};
|
||||
|
||||
export const validateLicense = async (
|
||||
licenseKey: string,
|
||||
serverIp?: string,
|
||||
) => {
|
||||
const license = await db.query.licenses.findFirst({
|
||||
where: eq(licenses.licenseKey, licenseKey),
|
||||
});
|
||||
|
||||
if (!license) {
|
||||
return { isValid: false, error: "License not found" };
|
||||
}
|
||||
|
||||
if (license.status !== "active") {
|
||||
return { isValid: false, error: "License is not active" };
|
||||
}
|
||||
|
||||
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() })
|
||||
.where(eq(licenses.id, license.id));
|
||||
|
||||
return { isValid: true, license };
|
||||
};
|
||||
|
||||
export const activateLicense = async (licenseKey: string, serverIp: string) => {
|
||||
const license = await db.query.licenses.findFirst({
|
||||
where: eq(licenses.licenseKey, licenseKey),
|
||||
});
|
||||
|
||||
if (!license) {
|
||||
throw new Error("License not found");
|
||||
}
|
||||
|
||||
if (license.status !== "active") {
|
||||
throw new Error("License is not active");
|
||||
}
|
||||
|
||||
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 (license.serverIp && license.serverIp !== serverIp) {
|
||||
throw new Error("License is already activated on a different server");
|
||||
}
|
||||
|
||||
// Activate the license with the server IP
|
||||
const updatedLicense = await db
|
||||
.update(licenses)
|
||||
.set({
|
||||
serverIp,
|
||||
activatedAt: new Date(),
|
||||
lastVerifiedAt: new Date(),
|
||||
})
|
||||
.where(eq(licenses.id, license.id))
|
||||
.returning();
|
||||
|
||||
return updatedLicense[0];
|
||||
};
|
||||
303
apps/licenses/templates/emails/license-email.tsx
Normal file
303
apps/licenses/templates/emails/license-email.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
Button,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
interface LicenseEmailProps {
|
||||
customerName: string;
|
||||
licenseKey: string;
|
||||
productName: string;
|
||||
expirationDate: Date;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
const baseUrl = "https://dokploy.com";
|
||||
|
||||
export const LicenseEmail = ({
|
||||
customerName = "John Doe",
|
||||
licenseKey = "1234567890",
|
||||
productName = "Dokploy",
|
||||
expirationDate = new Date(),
|
||||
features = ["Feature 1", "Feature 2", "Feature 3"],
|
||||
}: LicenseEmailProps): React.ReactElement => {
|
||||
const formattedDate = expirationDate.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Your Dokploy License Key is Here! 🚀</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Section style={heroSection}>
|
||||
<Img
|
||||
src={`${baseUrl}/og.png`}
|
||||
width="180"
|
||||
height="auto"
|
||||
alt="Dokploy"
|
||||
style={{ borderRadius: "10px", margin: "0 auto" }}
|
||||
/>
|
||||
<Heading style={heroTitle}>
|
||||
Welcome to the Future of Deployment
|
||||
</Heading>
|
||||
</Section>
|
||||
|
||||
<Section style={mainContent}>
|
||||
<Text style={greeting}>Hi {customerName},</Text>
|
||||
<Text style={text}>
|
||||
Thank you for choosing {productName}! We're excited to have you on
|
||||
board. Your premium license key is ready to unlock all the
|
||||
powerful features:
|
||||
</Text>
|
||||
|
||||
<Section style={licenseContainer}>
|
||||
<Text style={licenseLabel}>Your License Key</Text>
|
||||
<Text style={licenseKeyStyle}>{licenseKey}</Text>
|
||||
</Section>
|
||||
|
||||
<Section style={validitySection}>
|
||||
<Text style={validityText}>
|
||||
🗓️ Next billing date: <strong>{formattedDate}</strong>
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Section style={featuresContainer}>
|
||||
<Heading as="h2" style={h2}>
|
||||
🎉 Your Premium Features
|
||||
</Heading>
|
||||
<div style={featureGrid}>
|
||||
{features.map((feature, index) => (
|
||||
<Text key={index} style={featureItem}>
|
||||
<span style={checkmark}>✓</span> {feature}
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section style={activationSection}>
|
||||
<Heading as="h2" style={h2}>
|
||||
Getting Started
|
||||
</Heading>
|
||||
<div style={stepsContainer}>
|
||||
<Text style={steps}>
|
||||
1. Go to your Dokploy dashboard
|
||||
<br />
|
||||
2. Navigate to Settings → License
|
||||
<br />
|
||||
3. Enter your license key
|
||||
<br />
|
||||
4. Click "Activate License"
|
||||
</Text>
|
||||
</div>
|
||||
<Button href="https://dokploy.com/dashboard" style={ctaButton}>
|
||||
Activate Your License
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Hr style={hr} />
|
||||
|
||||
<Section style={supportSection}>
|
||||
<Text style={supportText}>
|
||||
Need help? Our support team is ready to assist you.
|
||||
<br />
|
||||
<Link style={link} href="mailto:support@dokploy.com">
|
||||
support@dokploy.com
|
||||
</Link>
|
||||
</Text>
|
||||
</Section>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
const main = {
|
||||
backgroundColor: "#f6f9fc",
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||
};
|
||||
|
||||
const container = {
|
||||
margin: "0 auto",
|
||||
padding: "40px 0",
|
||||
maxWidth: "600px",
|
||||
};
|
||||
|
||||
const heroSection = {
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: "8px",
|
||||
padding: "40px 20px",
|
||||
textAlign: "center" as const,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
|
||||
};
|
||||
|
||||
const heroTitle = {
|
||||
color: "#1a1a1a",
|
||||
fontSize: "28px",
|
||||
fontWeight: "800",
|
||||
lineHeight: "1.3",
|
||||
margin: "20px 0 0",
|
||||
padding: "0",
|
||||
};
|
||||
|
||||
const mainContent = {
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: "8px",
|
||||
marginTop: "24px",
|
||||
padding: "40px",
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
|
||||
};
|
||||
|
||||
const greeting = {
|
||||
fontSize: "20px",
|
||||
lineHeight: "1.3",
|
||||
fontWeight: "600",
|
||||
color: "#1a1a1a",
|
||||
margin: "0 0 20px",
|
||||
};
|
||||
|
||||
const text = {
|
||||
color: "#4a5568",
|
||||
fontSize: "16px",
|
||||
lineHeight: "1.6",
|
||||
margin: "0 0 24px",
|
||||
};
|
||||
|
||||
const licenseContainer = {
|
||||
background: "linear-gradient(135deg, #2563eb 0%, #1e40af 100%)",
|
||||
borderRadius: "12px",
|
||||
padding: "24px",
|
||||
margin: "32px 0",
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
const licenseLabel = {
|
||||
color: "#ffffff",
|
||||
fontSize: "14px",
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "1px",
|
||||
margin: "0 0 12px",
|
||||
};
|
||||
|
||||
const licenseKeyStyle = {
|
||||
fontFamily: "monospace",
|
||||
fontSize: "24px",
|
||||
color: "#ffffff",
|
||||
margin: "0",
|
||||
wordBreak: "break-all" as const,
|
||||
fontWeight: "600",
|
||||
};
|
||||
|
||||
const validitySection = {
|
||||
textAlign: "center" as const,
|
||||
margin: "24px 0",
|
||||
};
|
||||
|
||||
const validityText = {
|
||||
color: "#4a5568",
|
||||
fontSize: "16px",
|
||||
};
|
||||
|
||||
const featuresContainer = {
|
||||
margin: "40px 0",
|
||||
};
|
||||
|
||||
const featureGrid = {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr",
|
||||
gap: "12px",
|
||||
};
|
||||
|
||||
const featureItem = {
|
||||
color: "#4a5568",
|
||||
fontSize: "16px",
|
||||
lineHeight: "1.5",
|
||||
margin: "0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
};
|
||||
|
||||
const checkmark = {
|
||||
color: "#2563eb",
|
||||
fontWeight: "bold",
|
||||
marginRight: "12px",
|
||||
fontSize: "18px",
|
||||
};
|
||||
|
||||
const h2 = {
|
||||
color: "#1a1a1a",
|
||||
fontSize: "20px",
|
||||
fontWeight: "600",
|
||||
margin: "0 0 20px",
|
||||
padding: "0",
|
||||
};
|
||||
|
||||
const activationSection = {
|
||||
backgroundColor: "#f8fafc",
|
||||
borderRadius: "8px",
|
||||
padding: "24px",
|
||||
margin: "32px 0",
|
||||
};
|
||||
|
||||
const stepsContainer = {
|
||||
margin: "20px 0",
|
||||
};
|
||||
|
||||
const steps = {
|
||||
color: "#4a5568",
|
||||
fontSize: "16px",
|
||||
lineHeight: "1.8",
|
||||
margin: "0",
|
||||
};
|
||||
|
||||
const ctaButton = {
|
||||
backgroundColor: "#2563eb",
|
||||
borderRadius: "6px",
|
||||
color: "#ffffff",
|
||||
fontSize: "16px",
|
||||
fontWeight: "600",
|
||||
textDecoration: "none",
|
||||
textAlign: "center" as const,
|
||||
display: "inline-block",
|
||||
padding: "12px 24px",
|
||||
margin: "20px 0 0",
|
||||
};
|
||||
|
||||
const hr = {
|
||||
borderColor: "#e2e8f0",
|
||||
margin: "40px 0",
|
||||
};
|
||||
|
||||
const supportSection = {
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
const supportText = {
|
||||
color: "#64748b",
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.5",
|
||||
margin: "0",
|
||||
};
|
||||
|
||||
const link = {
|
||||
color: "#2563eb",
|
||||
textDecoration: "none",
|
||||
fontWeight: "500",
|
||||
};
|
||||
|
||||
export default LicenseEmail;
|
||||
292
apps/licenses/templates/emails/resend-license-email.tsx
Normal file
292
apps/licenses/templates/emails/resend-license-email.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
Button,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
interface ResendLicenseEmailProps {
|
||||
customerName: string;
|
||||
licenseKey: string;
|
||||
productName: string;
|
||||
expirationDate: Date;
|
||||
requestDate?: Date;
|
||||
}
|
||||
|
||||
const baseUrl = "https://dokploy.com";
|
||||
|
||||
export const ResendLicenseEmail = ({
|
||||
customerName = "John Doe",
|
||||
licenseKey = "1234567890",
|
||||
productName = "Dokploy",
|
||||
expirationDate = new Date(),
|
||||
requestDate = new Date(),
|
||||
}: ResendLicenseEmailProps): React.ReactElement => {
|
||||
const formattedDate = expirationDate.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const formattedRequestDate = requestDate.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>Your Requested Dokploy License Key 🔑</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Section style={heroSection}>
|
||||
<Img
|
||||
src={`${baseUrl}/og.png`}
|
||||
width="180"
|
||||
height="auto"
|
||||
alt="Dokploy"
|
||||
style={{ borderRadius: "10px", margin: "0 auto" }}
|
||||
/>
|
||||
<Heading style={heroTitle}>Here's Your License Key</Heading>
|
||||
</Section>
|
||||
|
||||
<Section style={mainContent}>
|
||||
<Text style={greeting}>Hi {customerName},</Text>
|
||||
<Text style={text}>
|
||||
As requested on {formattedRequestDate}, here is your {productName}{" "}
|
||||
license key. This is the same active license key associated with
|
||||
your account:
|
||||
</Text>
|
||||
|
||||
<Section style={licenseContainer}>
|
||||
<Text style={licenseLabel}>Your Active License Key</Text>
|
||||
<Text style={licenseKeyStyle}>{licenseKey}</Text>
|
||||
</Section>
|
||||
|
||||
<Section style={validitySection}>
|
||||
<Text style={validityText}>
|
||||
🗓️ Valid until: <strong>{formattedDate}</strong>
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Section style={activationSection}>
|
||||
<Heading as="h2" style={h2}>
|
||||
Quick Activation Guide
|
||||
</Heading>
|
||||
<div style={stepsContainer}>
|
||||
<Text style={steps}>
|
||||
1. Go to your Dokploy dashboard
|
||||
<br />
|
||||
2. Navigate to Settings → License
|
||||
<br />
|
||||
3. Enter your license key above
|
||||
<br />
|
||||
4. Click "Activate License"
|
||||
</Text>
|
||||
</div>
|
||||
<Button href="https://dokploy.com/dashboard" style={ctaButton}>
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<Hr style={hr} />
|
||||
|
||||
<Section style={securitySection}>
|
||||
<Text style={securityText}>
|
||||
🔒 For security: If you didn't request this license key, please
|
||||
contact our support team immediately.
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Section style={supportSection}>
|
||||
<Text style={supportText}>
|
||||
Need help? Our support team is ready to assist you.
|
||||
<br />
|
||||
<Link style={link} href="mailto:support@dokploy.com">
|
||||
support@dokploy.com
|
||||
</Link>
|
||||
</Text>
|
||||
</Section>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
const main = {
|
||||
backgroundColor: "#f6f9fc",
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||
};
|
||||
|
||||
const container = {
|
||||
margin: "0 auto",
|
||||
padding: "40px 0",
|
||||
maxWidth: "600px",
|
||||
};
|
||||
|
||||
const heroSection = {
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: "8px",
|
||||
padding: "40px 20px",
|
||||
textAlign: "center" as const,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
|
||||
};
|
||||
|
||||
const heroTitle = {
|
||||
color: "#1a1a1a",
|
||||
fontSize: "28px",
|
||||
fontWeight: "800",
|
||||
lineHeight: "1.3",
|
||||
margin: "20px 0 0",
|
||||
padding: "0",
|
||||
};
|
||||
|
||||
const mainContent = {
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: "8px",
|
||||
marginTop: "24px",
|
||||
padding: "40px",
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
|
||||
};
|
||||
|
||||
const greeting = {
|
||||
fontSize: "20px",
|
||||
lineHeight: "1.3",
|
||||
fontWeight: "600",
|
||||
color: "#1a1a1a",
|
||||
margin: "0 0 20px",
|
||||
};
|
||||
|
||||
const text = {
|
||||
color: "#4a5568",
|
||||
fontSize: "16px",
|
||||
lineHeight: "1.6",
|
||||
margin: "0 0 24px",
|
||||
};
|
||||
|
||||
const licenseContainer = {
|
||||
background: "linear-gradient(135deg, #2563eb 0%, #1e40af 100%)",
|
||||
borderRadius: "12px",
|
||||
padding: "24px",
|
||||
margin: "32px 0",
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
const licenseLabel = {
|
||||
color: "#ffffff",
|
||||
fontSize: "14px",
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "1px",
|
||||
margin: "0 0 12px",
|
||||
};
|
||||
|
||||
const licenseKeyStyle = {
|
||||
fontFamily: "monospace",
|
||||
fontSize: "24px",
|
||||
color: "#ffffff",
|
||||
margin: "0",
|
||||
wordBreak: "break-all" as const,
|
||||
fontWeight: "600",
|
||||
};
|
||||
|
||||
const validitySection = {
|
||||
textAlign: "center" as const,
|
||||
margin: "24px 0",
|
||||
};
|
||||
|
||||
const validityText = {
|
||||
color: "#4a5568",
|
||||
fontSize: "16px",
|
||||
};
|
||||
|
||||
const h2 = {
|
||||
color: "#1a1a1a",
|
||||
fontSize: "20px",
|
||||
fontWeight: "600",
|
||||
margin: "0 0 20px",
|
||||
padding: "0",
|
||||
};
|
||||
|
||||
const activationSection = {
|
||||
backgroundColor: "#f8fafc",
|
||||
borderRadius: "8px",
|
||||
padding: "24px",
|
||||
margin: "32px 0",
|
||||
};
|
||||
|
||||
const stepsContainer = {
|
||||
margin: "20px 0",
|
||||
};
|
||||
|
||||
const steps = {
|
||||
color: "#4a5568",
|
||||
fontSize: "16px",
|
||||
lineHeight: "1.8",
|
||||
margin: "0",
|
||||
};
|
||||
|
||||
const ctaButton = {
|
||||
backgroundColor: "#2563eb",
|
||||
borderRadius: "6px",
|
||||
color: "#ffffff",
|
||||
fontSize: "16px",
|
||||
fontWeight: "600",
|
||||
textDecoration: "none",
|
||||
textAlign: "center" as const,
|
||||
display: "inline-block",
|
||||
padding: "12px 24px",
|
||||
margin: "20px 0 0",
|
||||
};
|
||||
|
||||
const hr = {
|
||||
borderColor: "#e2e8f0",
|
||||
margin: "40px 0",
|
||||
};
|
||||
|
||||
const securitySection = {
|
||||
backgroundColor: "#fff5f5",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
margin: "0 0 32px 0",
|
||||
};
|
||||
|
||||
const securityText = {
|
||||
color: "#e53e3e",
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.5",
|
||||
margin: "0",
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
const supportSection = {
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
const supportText = {
|
||||
color: "#64748b",
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.5",
|
||||
margin: "0",
|
||||
};
|
||||
|
||||
const link = {
|
||||
color: "#2563eb",
|
||||
textDecoration: "none",
|
||||
fontWeight: "500",
|
||||
};
|
||||
|
||||
export default ResendLicenseEmail;
|
||||
BIN
apps/licenses/templates/emails/static/vercel-user.png
Normal file
BIN
apps/licenses/templates/emails/static/vercel-user.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
3495
apps/licenses/templates/package-lock.json
generated
Normal file
3495
apps/licenses/templates/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
apps/licenses/templates/package.json
Normal file
20
apps/licenses/templates/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "react-email-starter",
|
||||
"version": "0.1.10",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "email build",
|
||||
"dev": "email dev",
|
||||
"export": "email export"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "0.0.34",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.2.33",
|
||||
"@types/react-dom": "18.2.14",
|
||||
"react-email": "3.0.7"
|
||||
}
|
||||
}
|
||||
27
apps/licenses/templates/readme.md
Normal file
27
apps/licenses/templates/readme.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# React Email Starter
|
||||
|
||||
A live preview right in your browser so you don't need to keep sending real emails during development.
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, install the dependencies:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
# or
|
||||
yarn
|
||||
```
|
||||
|
||||
Then, run the development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
@@ -2,7 +2,9 @@
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
@@ -14,5 +16,6 @@
|
||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
236
pnpm-lock.yaml
generated
236
pnpm-lock.yaml
generated
@@ -303,10 +303,10 @@ importers:
|
||||
version: 16.4.5
|
||||
drizzle-orm:
|
||||
specifier: ^0.39.1
|
||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
drizzle-zod:
|
||||
specifier: 0.5.1
|
||||
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
|
||||
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
|
||||
fancy-ansi:
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3
|
||||
@@ -530,27 +530,45 @@ importers:
|
||||
|
||||
apps/licenses:
|
||||
dependencies:
|
||||
'@dokploy/server':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/server
|
||||
'@hono/node-server':
|
||||
specifier: ^1.12.1
|
||||
version: 1.12.1
|
||||
'@hono/zod-validator':
|
||||
specifier: 0.3.0
|
||||
version: 0.3.0(hono@4.5.8)(zod@3.24.1)
|
||||
'@react-email/components':
|
||||
specifier: ^0.0.21
|
||||
version: 0.0.21(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@react-email/render':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@types/pg':
|
||||
specifier: ^8.11.11
|
||||
version: 8.11.11
|
||||
dotenv:
|
||||
specifier: ^16.3.1
|
||||
version: 16.4.5
|
||||
drizzle-orm:
|
||||
specifier: ^0.39.1
|
||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
hono:
|
||||
specifier: ^4.5.8
|
||||
version: 4.5.8
|
||||
nodemailer:
|
||||
specifier: 6.9.14
|
||||
version: 6.9.14
|
||||
pg:
|
||||
specifier: ^8.14.1
|
||||
version: 8.14.1
|
||||
pino:
|
||||
specifier: 9.4.0
|
||||
version: 9.4.0
|
||||
pino-pretty:
|
||||
specifier: 11.2.2
|
||||
version: 11.2.2
|
||||
postgres:
|
||||
specifier: 3.4.4
|
||||
version: 3.4.4
|
||||
react:
|
||||
specifier: 18.2.0
|
||||
version: 18.2.0
|
||||
@@ -567,12 +585,18 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^20.11.17
|
||||
version: 20.14.10
|
||||
'@types/nodemailer':
|
||||
specifier: ^6.4.16
|
||||
version: 6.4.16
|
||||
'@types/react':
|
||||
specifier: 18.3.5
|
||||
version: 18.3.5
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
drizzle-kit:
|
||||
specifier: ^0.30.4
|
||||
version: 0.30.4
|
||||
tsx:
|
||||
specifier: ^4.7.1
|
||||
version: 4.16.2
|
||||
@@ -599,7 +623,7 @@ importers:
|
||||
version: 16.4.5
|
||||
drizzle-orm:
|
||||
specifier: ^0.39.1
|
||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
hono:
|
||||
specifier: ^4.5.8
|
||||
version: 4.5.8
|
||||
@@ -717,13 +741,13 @@ importers:
|
||||
version: 16.4.5
|
||||
drizzle-dbml-generator:
|
||||
specifier: 0.10.0
|
||||
version: 0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))
|
||||
version: 0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))
|
||||
drizzle-orm:
|
||||
specifier: ^0.39.1
|
||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
drizzle-zod:
|
||||
specifier: 0.5.1
|
||||
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
|
||||
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
|
||||
hi-base32:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1
|
||||
@@ -3318,6 +3342,13 @@ packages:
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
|
||||
'@react-email/render@1.0.5':
|
||||
resolution: {integrity: sha512-CA69HYXPk21HhtAXATIr+9JJwpDNmAFCvdMUjWmeoD1+KhJ9NAxusMRxKNeibdZdslmq3edaeOKGbdQ9qjK8LQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
react: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
|
||||
'@react-email/row@0.0.8':
|
||||
resolution: {integrity: sha512-JsB6pxs/ZyjYpEML3nbwJRGAerjcN/Pa/QG48XUwnT/MioDWrUuyQuefw+CwCrSUZ2P1IDrv2tUD3/E3xzcoKw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -3779,6 +3810,9 @@ packages:
|
||||
'@types/nodemailer@6.4.16':
|
||||
resolution: {integrity: sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==}
|
||||
|
||||
'@types/pg@8.11.11':
|
||||
resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==}
|
||||
|
||||
'@types/prop-types@15.7.12':
|
||||
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
|
||||
|
||||
@@ -6187,6 +6221,9 @@ packages:
|
||||
resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
obuf@1.1.2:
|
||||
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
|
||||
|
||||
octokit@3.1.2:
|
||||
resolution: {integrity: sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -6335,6 +6372,48 @@ packages:
|
||||
peberminta@0.9.0:
|
||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||
|
||||
pg-cloudflare@1.1.1:
|
||||
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
|
||||
|
||||
pg-connection-string@2.7.0:
|
||||
resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==}
|
||||
|
||||
pg-int8@1.0.1:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
pg-numeric@1.0.2:
|
||||
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg-pool@3.8.0:
|
||||
resolution: {integrity: sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
|
||||
pg-protocol@1.8.0:
|
||||
resolution: {integrity: sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==}
|
||||
|
||||
pg-types@2.2.0:
|
||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg-types@4.0.2:
|
||||
resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
pg@8.14.1:
|
||||
resolution: {integrity: sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
peerDependencies:
|
||||
pg-native: '>=3.0.1'
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
|
||||
pgpass@1.0.5:
|
||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||
|
||||
picocolors@1.0.1:
|
||||
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
|
||||
|
||||
@@ -6429,6 +6508,41 @@ packages:
|
||||
resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postgres-array@2.0.0:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postgres-array@3.0.4:
|
||||
resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postgres-bytea@1.0.0:
|
||||
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-bytea@3.0.0:
|
||||
resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
postgres-date@1.0.7:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-date@2.1.0:
|
||||
resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-interval@3.0.0:
|
||||
resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postgres-range@1.1.4:
|
||||
resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==}
|
||||
|
||||
postgres@3.4.4:
|
||||
resolution: {integrity: sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -6438,6 +6552,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
prettier@3.4.2:
|
||||
resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-format@29.7.0:
|
||||
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -9953,6 +10072,14 @@ snapshots:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-promise-suspense: 0.3.4
|
||||
|
||||
'@react-email/render@1.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
html-to-text: 9.0.5
|
||||
prettier: 3.4.2
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-promise-suspense: 0.3.4
|
||||
|
||||
'@react-email/row@0.0.8(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
@@ -10627,6 +10754,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
|
||||
'@types/pg@8.11.11':
|
||||
dependencies:
|
||||
'@types/node': 20.14.10
|
||||
pg-protocol: 1.8.0
|
||||
pg-types: 4.0.2
|
||||
|
||||
'@types/prop-types@15.7.12': {}
|
||||
|
||||
'@types/qrcode@1.5.5':
|
||||
@@ -11591,9 +11724,9 @@ snapshots:
|
||||
|
||||
drange@1.1.1: {}
|
||||
|
||||
drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)):
|
||||
drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)):
|
||||
dependencies:
|
||||
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
|
||||
drizzle-kit@0.30.4:
|
||||
dependencies:
|
||||
@@ -11604,18 +11737,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7):
|
||||
drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7):
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@types/pg': 8.11.11
|
||||
'@types/react': 18.3.5
|
||||
kysely: 0.27.6
|
||||
pg: 8.14.1
|
||||
postgres: 3.4.4
|
||||
react: 18.2.0
|
||||
sqlite3: 5.1.7
|
||||
|
||||
drizzle-zod@0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8):
|
||||
drizzle-zod@0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8):
|
||||
dependencies:
|
||||
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(@types/react@18.3.5)(kysely@0.27.6)(pg@8.14.1)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||
zod: 3.23.8
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
@@ -13284,6 +13419,8 @@ snapshots:
|
||||
|
||||
object-inspect@1.13.2: {}
|
||||
|
||||
obuf@1.1.2: {}
|
||||
|
||||
octokit@3.1.2:
|
||||
dependencies:
|
||||
'@octokit/app': 14.1.0
|
||||
@@ -13435,6 +13572,53 @@ snapshots:
|
||||
|
||||
peberminta@0.9.0: {}
|
||||
|
||||
pg-cloudflare@1.1.1:
|
||||
optional: true
|
||||
|
||||
pg-connection-string@2.7.0: {}
|
||||
|
||||
pg-int8@1.0.1: {}
|
||||
|
||||
pg-numeric@1.0.2: {}
|
||||
|
||||
pg-pool@3.8.0(pg@8.14.1):
|
||||
dependencies:
|
||||
pg: 8.14.1
|
||||
|
||||
pg-protocol@1.8.0: {}
|
||||
|
||||
pg-types@2.2.0:
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
postgres-array: 2.0.0
|
||||
postgres-bytea: 1.0.0
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
|
||||
pg-types@4.0.2:
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
pg-numeric: 1.0.2
|
||||
postgres-array: 3.0.4
|
||||
postgres-bytea: 3.0.0
|
||||
postgres-date: 2.1.0
|
||||
postgres-interval: 3.0.0
|
||||
postgres-range: 1.1.4
|
||||
|
||||
pg@8.14.1:
|
||||
dependencies:
|
||||
pg-connection-string: 2.7.0
|
||||
pg-pool: 3.8.0(pg@8.14.1)
|
||||
pg-protocol: 1.8.0
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
optionalDependencies:
|
||||
pg-cloudflare: 1.1.1
|
||||
|
||||
pgpass@1.0.5:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
picocolors@1.0.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
@@ -13544,6 +13728,28 @@ snapshots:
|
||||
picocolors: 1.0.1
|
||||
source-map-js: 1.2.0
|
||||
|
||||
postgres-array@2.0.0: {}
|
||||
|
||||
postgres-array@3.0.4: {}
|
||||
|
||||
postgres-bytea@1.0.0: {}
|
||||
|
||||
postgres-bytea@3.0.0:
|
||||
dependencies:
|
||||
obuf: 1.1.2
|
||||
|
||||
postgres-date@1.0.7: {}
|
||||
|
||||
postgres-date@2.1.0: {}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
postgres-interval@3.0.0: {}
|
||||
|
||||
postgres-range@1.1.4: {}
|
||||
|
||||
postgres@3.4.4: {}
|
||||
|
||||
prebuild-install@7.1.2:
|
||||
@@ -13562,6 +13768,8 @@ snapshots:
|
||||
tunnel-agent: 0.6.0
|
||||
optional: true
|
||||
|
||||
prettier@3.4.2: {}
|
||||
|
||||
pretty-format@29.7.0:
|
||||
dependencies:
|
||||
'@jest/schemas': 29.6.3
|
||||
|
||||
Reference in New Issue
Block a user