From 1acd3304629bceaba5cc9ef7934ed2a1b2852c23 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Sun, 23 Mar 2025 19:40:54 -0600
Subject: [PATCH] feat(licenses): implement license deactivation and validation
features
---
.../settings/enable-paid-features.tsx | 82 +++++++++++++++++--
apps/dokploy/server/api/routers/user.ts | 51 ++++++++++--
apps/dokploy/server/utils/validate-license.ts | 16 ++++
apps/licenses/src/api/license.ts | 57 ++++++++++++-
apps/licenses/src/utils/license.ts | 59 +++++++++++--
5 files changed, 239 insertions(+), 26 deletions(-)
diff --git a/apps/dokploy/components/dashboard/settings/enable-paid-features.tsx b/apps/dokploy/components/dashboard/settings/enable-paid-features.tsx
index c2c6ec2ac..1761d14b5 100644
--- a/apps/dokploy/components/dashboard/settings/enable-paid-features.tsx
+++ b/apps/dokploy/components/dashboard/settings/enable-paid-features.tsx
@@ -18,6 +18,10 @@ export const EnablePaidFeatures = () => {
const { data, refetch } = api.user.get.useQuery();
const [isLoading, setIsLoading] = useState(false);
const { mutateAsync: saveLicense } = api.user.saveLicense.useMutation();
+ const { mutateAsync: deactivateLicense } =
+ api.user.deactivateLicense.useMutation();
+ const { mutateAsync: validateLicense } =
+ api.user.validateLicense.useMutation();
const { mutateAsync: update } = api.user.update.useMutation();
const [licenseKey, setLicenseKey] = useState("");
@@ -27,7 +31,7 @@ export const EnablePaidFeatures = () => {
}
}, [data?.user?.enablePaidFeatures]);
- const handleValidateLicense = async () => {
+ const handleSaveLicense = async () => {
if (!licenseKey) {
toast.error("Please enter a license key");
return;
@@ -47,6 +51,30 @@ export const EnablePaidFeatures = () => {
.finally(() => {
setIsLoading(false);
});
+
+ refetch();
+ };
+
+ const handleValidateLicense = async () => {
+ if (!licenseKey) {
+ toast.error("Please enter a license key");
+ return;
+ }
+ setIsLoading(true);
+ await validateLicense({
+ licenseKey,
+ })
+ .then(() => {
+ toast.success("License validated successfully");
+ })
+ .catch((e) => {
+ toast.error("Error validating license", {
+ description: e.message,
+ });
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
};
return (
@@ -103,17 +131,55 @@ export const EnablePaidFeatures = () => {
setLicenseKey(e.target.value)}
className="w-full"
/>
-
+ {!data?.user?.licenseKey ? (
+
+ ) : (
+
+
+
+
+ )}
)}
diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts
index 2676333d9..ec76f9948 100644
--- a/apps/dokploy/server/api/routers/user.ts
+++ b/apps/dokploy/server/api/routers/user.ts
@@ -32,6 +32,7 @@ import {
import {
validateLicense,
activateLicense,
+ deactivateLicense,
} from "@/server/utils/validate-license";
const apiCreateApiKey = z.object({
name: z.string().min(1),
@@ -151,23 +152,63 @@ export const userRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
const owner = await findUserById(ctx.user.ownerId);
- const result = await validateLicense(
+
+ const result = await activateLicense(
input.licenseKey,
owner?.serverIp || "",
);
- if (!result.isValid) {
+
+ if (!result.success) {
throw new TRPCError({
- code: "UNAUTHORIZED",
+ code: "BAD_REQUEST",
message: result.error,
});
}
- await activateLicense(input.licenseKey, owner?.serverIp || "");
-
await updateUser(ctx.user.id, {
licenseKey: input.licenseKey,
});
+ return result;
+ }),
+ deactivateLicense: adminProcedure
+ .input(z.object({ licenseKey: z.string().min(1) }))
+ .mutation(async ({ input, ctx }) => {
+ const owner = await findUserById(ctx.user.ownerId);
+ const result = await deactivateLicense(
+ input.licenseKey,
+ owner?.serverIp || "",
+ );
+
+ if (!result.success) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: result.error,
+ });
+ }
+
+ await updateUser(ctx.user.id, {
+ licenseKey: null,
+ });
+
+ return result;
+ }),
+ validateLicense: adminProcedure
+ .input(z.object({ licenseKey: z.string().min(1) }))
+ .mutation(async ({ input, ctx }) => {
+ const owner = await findUserById(ctx.user.ownerId);
+ const result = await validateLicense(
+ input.licenseKey,
+ owner?.serverIp || "",
+ );
+
+ if (!result.success) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: result.error,
+ });
+ }
+
return result;
}),
getUserByToken: publicProcedure
diff --git a/apps/dokploy/server/utils/validate-license.ts b/apps/dokploy/server/utils/validate-license.ts
index 6f7a07b72..5475e8a0a 100644
--- a/apps/dokploy/server/utils/validate-license.ts
+++ b/apps/dokploy/server/utils/validate-license.ts
@@ -29,3 +29,19 @@ export const activateLicense = async (licenseKey: string, serverIp: string) => {
return data;
};
+
+export const deactivateLicense = async (
+ licenseKey: string,
+ serverIp: string,
+) => {
+ const response = await fetch(`${licensesUrl}/api/license/deactivate`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ licenseKey, serverIp }),
+ });
+ const data = await response.json();
+
+ return data;
+};
diff --git a/apps/licenses/src/api/license.ts b/apps/licenses/src/api/license.ts
index 30013de5a..b12399908 100644
--- a/apps/licenses/src/api/license.ts
+++ b/apps/licenses/src/api/license.ts
@@ -1,15 +1,21 @@
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
-import { activateLicense, validateLicense } from "../utils/license";
+import {
+ activateLicense,
+ deactivateLicense,
+ validateLicense,
+ cleanLicense,
+} from "../utils/license";
import { logger } from "../logger";
-import { eq } from "drizzle-orm";
-import { users } from "../schema";
+import { eq, desc } from "drizzle-orm";
+import { users, licenses } from "../schema";
import { db } from "../db";
import { transporter } from "../email";
import { nanoid } from "nanoid";
import { stripe } from "../stripe";
import type Stripe from "stripe";
+import { getLicenseTypeFromPriceId } from "../utils";
const validateSchema = z.object({
licenseKey: z.string(),
serverIp: z.string(),
@@ -28,7 +34,7 @@ licenseRouter.post(
return c.json(result);
} catch (error) {
logger.error("Error validating license:", { error });
- return c.json({ isValid: false, error: "Error validating license" }, 500);
+ return c.json({ success: false, error: "Error validating license" }, 500);
}
},
);
@@ -52,6 +58,43 @@ licenseRouter.post(
},
);
+licenseRouter.post(
+ "/deactivate",
+ zValidator("json", validateSchema),
+ async (c) => {
+ const { licenseKey, serverIp } = c.req.valid("json");
+
+ try {
+ const license = await deactivateLicense(licenseKey, serverIp);
+ return c.json({ success: true, license });
+ } catch (error) {
+ logger.error("Error deactivating license:", error);
+ return c.json(
+ { success: false, error: "Error deactivating license" },
+ 500,
+ );
+ }
+ },
+);
+
+licenseRouter.post(
+ "/remove-server",
+ zValidator(
+ "json",
+ z.object({ licenseKey: z.string().min(1), serverIp: z.string().min(1) }),
+ ),
+ async (c) => {
+ const { licenseKey, serverIp } = c.req.valid("json");
+
+ try {
+ const license = await cleanLicense(licenseKey, serverIp);
+ return c.json({ success: true, license });
+ } catch (error) {
+ logger.error("Error cleaning license:", error);
+ return c.json({ success: false, error: "Error cleaning license" }, 500);
+ }
+ },
+);
// router.post("/resend-license", zValidator("json", resendSchema), async (c) => {
// const { licenseKey } = c.req.valid("json");
@@ -178,6 +221,7 @@ licenseRouter.get(
with: {
licenses: true,
},
+ orderBy: desc(licenses.createdAt),
});
if (!user) {
@@ -202,11 +246,16 @@ licenseRouter.get(
(suscription) => suscription.id === license.stripeSubscriptionId,
);
+ const { type } = getLicenseTypeFromPriceId(
+ suscription?.items.data[0].price.id || "",
+ );
+
return {
license: license,
stripeSuscription: {
quantity: suscription?.items.data[0].quantity,
billingType: suscription?.items.data[0].price.recurring?.interval,
+ type: type,
},
};
});
diff --git a/apps/licenses/src/utils/license.ts b/apps/licenses/src/utils/license.ts
index ba97577e0..b92efbe56 100644
--- a/apps/licenses/src/utils/license.ts
+++ b/apps/licenses/src/utils/license.ts
@@ -69,7 +69,7 @@ export const validateLicense = async (licenseKey: string, serverIp: string) => {
});
if (!license) {
- return { isValid: false, error: "License not found" };
+ return { success: false, error: "License not found" };
}
const suscription = await stripe.subscriptions.retrieve(
@@ -80,7 +80,7 @@ export const validateLicense = async (licenseKey: string, serverIp: string) => {
if (currentServerQuantity >= serversQuantity) {
return {
- isValid: false,
+ success: false,
error:
"You have reached the maximum number of servers, please upgrade your license to add more servers",
};
@@ -88,13 +88,17 @@ export const validateLicense = async (licenseKey: string, serverIp: string) => {
if (suscription.status !== "active") {
return {
- isValid: false,
+ success: false,
error: `License is ${getLicenseStatus(suscription)}`,
};
}
if (license.serverIps && !license.serverIps.includes(serverIp)) {
- return { isValid: false, error: "Invalid server IP" };
+ return {
+ success: false,
+ error:
+ "This server is not authorized to use this license, please remove the current license from the UI, and activate a new one",
+ };
}
await db
@@ -102,7 +106,7 @@ export const validateLicense = async (licenseKey: string, serverIp: string) => {
.set({ lastVerifiedAt: new Date() })
.where(eq(licenses.id, license.id));
- return { isValid: true, license };
+ return { success: true, license };
};
export const activateLicense = async (licenseKey: string, serverIp: string) => {
@@ -130,10 +134,8 @@ export const activateLicense = async (licenseKey: string, serverIp: string) => {
);
}
- console.log("License", license.serverIps?.includes(serverIp));
-
- if (license.serverIps && !license.serverIps.includes(serverIp)) {
- throw new Error("License is already activated on a different server");
+ if (license.serverIps?.includes(serverIp)) {
+ return license;
}
// Activate the license with the server IP
@@ -150,6 +152,45 @@ export const activateLicense = async (licenseKey: string, serverIp: string) => {
return updatedLicense[0];
};
+export const deactivateLicense = 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");
+ }
+
+ const updatedLicense = await db
+ .update(licenses)
+ .set({ serverIps: license.serverIps?.filter((ip) => ip !== serverIp) })
+ .where(eq(licenses.id, license.id))
+ .returning();
+
+ return updatedLicense[0];
+};
+
+export const cleanLicense = 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");
+ }
+
+ const updatedLicense = await db
+ .update(licenses)
+ .set({ serverIps: license.serverIps?.filter((ip) => ip !== serverIp) })
+ .where(eq(licenses.id, license.id))
+ .returning();
+
+ return updatedLicense[0];
+};
+
export const getLicenseStatus = async (license: Stripe.Subscription) => {
if (license.status === "active") {
return "active";