From c8e6df4c29470103f6debc9b387a8639a4c10d36 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 23 Mar 2025 12:29:55 -0600 Subject: [PATCH] feat(licenses): enhance license validation and loading state management - Added loading state management in the EnablePaidFeatures component to improve user experience during license validation. - Updated validateLicense function to return detailed error messages for better feedback. - Modified user API to return validation results instead of a boolean, enhancing error handling. - Removed unused SQL files and updated package.json scripts for better development workflow. --- .../settings/enable-paid-features.tsx | 17 +- apps/dokploy/server/api/routers/user.ts | 8 +- apps/dokploy/server/utils/validate-license.ts | 7 +- apps/licenses/drizzle/0000_noisy_epoch.sql | 22 --- apps/licenses/drizzle/meta/0000_snapshot.json | 171 ------------------ apps/licenses/drizzle/meta/_journal.json | 10 +- apps/licenses/package.json | 1 + apps/licenses/src/index.ts | 14 +- apps/licenses/src/utils/license.ts | 2 + 9 files changed, 28 insertions(+), 224 deletions(-) delete mode 100644 apps/licenses/drizzle/0000_noisy_epoch.sql delete mode 100644 apps/licenses/drizzle/meta/0000_snapshot.json diff --git a/apps/dokploy/components/dashboard/settings/enable-paid-features.tsx b/apps/dokploy/components/dashboard/settings/enable-paid-features.tsx index 6b704029c..e3519edf5 100644 --- a/apps/dokploy/components/dashboard/settings/enable-paid-features.tsx +++ b/apps/dokploy/components/dashboard/settings/enable-paid-features.tsx @@ -16,6 +16,7 @@ import { useState, useEffect } from "react"; export const EnablePaidFeatures = () => { const { data, refetch } = api.user.get.useQuery(); + const [isLoading, setIsLoading] = useState(false); const { mutateAsync: validateLicense } = api.user.validateLicense.useMutation(); const { mutateAsync: update } = api.user.update.useMutation(); @@ -32,6 +33,7 @@ export const EnablePaidFeatures = () => { toast.error("Please enter a license key"); return; } + setIsLoading(true); await validateLicense({ licenseKey, }) @@ -40,7 +42,12 @@ export const EnablePaidFeatures = () => { }) .catch((e) => { console.error(e); - toast.error("Error validating license"); + toast.error("Error validating license", { + description: e.message, + }); + }) + .finally(() => { + setIsLoading(false); }); }; @@ -102,8 +109,12 @@ export const EnablePaidFeatures = () => { className="w-full" /> - )} diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index eb65c1d49..bc8fcae48 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -148,21 +148,21 @@ export const userRouter = createTRPCRouter({ ) .mutation(async ({ input, ctx }) => { const owner = await findUserById(ctx.user.ownerId); - const isValid = await validateLicense( + const result = await validateLicense( input.licenseKey, owner?.serverIp || "", ); - if (!isValid) { + if (!result.isValid) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "Invalid license key", + message: result.error, }); } await updateUser(ctx.user.id, { licenseKey: input.licenseKey, }); - return isValid; + return result; }), getUserByToken: publicProcedure .input(apiFindOneToken) diff --git a/apps/dokploy/server/utils/validate-license.ts b/apps/dokploy/server/utils/validate-license.ts index 8c786b36a..eb3b83075 100644 --- a/apps/dokploy/server/utils/validate-license.ts +++ b/apps/dokploy/server/utils/validate-license.ts @@ -1,9 +1,6 @@ const licensesUrl = process.env.LICENSES_URL || "http://localhost:4002"; -export const validateLicense = async ( - licenseKey: string, - serverIp: string, -): Promise => { +export const validateLicense = async (licenseKey: string, serverIp: string) => { const response = await fetch(`${licensesUrl}/api/validate`, { method: "POST", headers: { @@ -17,5 +14,5 @@ export const validateLicense = async ( console.log("Validation errors:", data.error.issues); } - return response.ok; + return data; }; diff --git a/apps/licenses/drizzle/0000_noisy_epoch.sql b/apps/licenses/drizzle/0000_noisy_epoch.sql deleted file mode 100644 index 3e41be2cf..000000000 --- a/apps/licenses/drizzle/0000_noisy_epoch.sql +++ /dev/null @@ -1,22 +0,0 @@ -CREATE TYPE "public"."billing_type" AS ENUM('monthly', 'annual');--> statement-breakpoint -CREATE TYPE "public"."license_status" AS ENUM('active', 'expired', 'cancelled', 'payment_pending');--> 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, - "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, - "stripeCustomerId" text NOT NULL, - "stripeSubscriptionId" text 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") -); diff --git a/apps/licenses/drizzle/meta/0000_snapshot.json b/apps/licenses/drizzle/meta/0000_snapshot.json deleted file mode 100644 index 5bf2b4325..000000000 --- a/apps/licenses/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "id": "5a996744-b11f-4f1a-b4b0-91f6bf5c2bed", - "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()" - }, - "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 - }, - "stripeCustomerId": { - "name": "stripeCustomerId", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "stripeSubscriptionId": { - "name": "stripeSubscriptionId", - "type": "text", - "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", - "payment_pending" - ] - }, - "public.license_type": { - "name": "license_type", - "schema": "public", - "values": [ - "basic", - "premium", - "business" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/apps/licenses/drizzle/meta/_journal.json b/apps/licenses/drizzle/meta/_journal.json index ff4b16fbb..eaa8fcf3b 100644 --- a/apps/licenses/drizzle/meta/_journal.json +++ b/apps/licenses/drizzle/meta/_journal.json @@ -1,13 +1,5 @@ { "version": "7", "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1742369437742, - "tag": "0000_noisy_epoch", - "breakpoints": true - } - ] + "entries": [] } \ No newline at end of file diff --git a/apps/licenses/package.json b/apps/licenses/package.json index 3ec4f59ce..ce2083ba6 100644 --- a/apps/licenses/package.json +++ b/apps/licenses/package.json @@ -9,6 +9,7 @@ "typecheck": "tsc --noEmit", "generate": "drizzle-kit generate", "drop": "drizzle-kit drop", + "push": "drizzle-kit push", "migrate": "tsx ./migrate.ts", "truncate": "tsx ./truncate.ts", "reset:all": "tsx ./truncate.ts && tsx ./migrate.ts", diff --git a/apps/licenses/src/index.ts b/apps/licenses/src/index.ts index f9c8ebf9b..163cfa58b 100644 --- a/apps/licenses/src/index.ts +++ b/apps/licenses/src/index.ts @@ -58,13 +58,12 @@ router.get("/health", async (c) => { router.post("/validate", zValidator("json", validateSchema), async (c) => { const { licenseKey, serverIp } = c.req.valid("json"); - console.log("Validating license", licenseKey, serverIp); - try { const result = await validateLicense(licenseKey, serverIp); + console.log("Result", result); return c.json(result); } catch (error) { - logger.error("Error validating license:", error); + logger.error("Error validating license:", { error }); return c.json({ isValid: false, error: "Error validating license" }, 500); } }); @@ -125,12 +124,7 @@ router.post("/resend-license", zValidator("json", resendSchema), async (c) => { customerName: license.email, }), ); - // await transporter.sendMail({ - // from: fromAddress, - // to: toAddresses.join(", "), - // subject, - // html: htmlContent, - // }); + await transporter.sendMail({ from: process.env.SMTP_FROM_ADDRESS, to: license.email, @@ -199,7 +193,7 @@ router.post("/stripe/webhook", async (c) => { billingType, email: session.customer_details?.email!, stripeCustomerId: customerResponse.id, - stripeSubscriptionId: session.id, + stripeSubscriptionId: session.subscription as string, }); console.log("License created", license); diff --git a/apps/licenses/src/utils/license.ts b/apps/licenses/src/utils/license.ts index ba8f10968..6cb5c1a59 100644 --- a/apps/licenses/src/utils/license.ts +++ b/apps/licenses/src/utils/license.ts @@ -60,6 +60,8 @@ export const validateLicense = async ( license.stripeSubscriptionId, ); + console.log("Suscription", suscription); + if (suscription.status !== "active") { return { isValid: false,