mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 21:55:24 +02:00
feat(licenses): implement license deactivation and validation features
This commit is contained in:
@@ -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 = () => {
|
||||
<Input
|
||||
placeholder="Enter your license key"
|
||||
value={licenseKey}
|
||||
disabled={data?.user?.licenseKey !== null}
|
||||
onChange={(e) => setLicenseKey(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleValidateLicense}
|
||||
variant="secondary"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Validating..." : "Validate"}
|
||||
</Button>
|
||||
{!data?.user?.licenseKey ? (
|
||||
<Button
|
||||
onClick={handleSaveLicense}
|
||||
variant="secondary"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Saving..." : "Save License"}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleValidateLicense}
|
||||
variant="secondary"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Validating..." : "Validate"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
deactivateLicense({
|
||||
licenseKey,
|
||||
})
|
||||
.then(() => {
|
||||
setLicenseKey("");
|
||||
toast.success("License removed successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error("Error removing license", {
|
||||
description: e.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}}
|
||||
variant="destructive"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user