mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-29 11:05:33 +02:00
Refactor license key management: remove legacy license key settings component, enhance license key validation and activation in the API, and implement new methods for activating and deactivating license keys.
This commit is contained in:
@@ -1,109 +0,0 @@
|
||||
import { Key } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
export function LicenseKeySettings() {
|
||||
const utils = api.useUtils();
|
||||
const { data, isLoading } = api.licenseKey.getEnterpriseSettings.useQuery();
|
||||
const { mutateAsync: updateEnterpriseSettings, isLoading: isSaving } =
|
||||
api.licenseKey.updateEnterpriseSettings.useMutation();
|
||||
|
||||
const [licenseKey, setLicenseKey] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.licenseKey !== undefined) {
|
||||
setLicenseKey(data.licenseKey ?? "");
|
||||
}
|
||||
}, [data?.licenseKey]);
|
||||
|
||||
const enabled = !!data?.enableEnterpriseFeatures;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg border p-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Key className="size-6 text-muted-foreground" />
|
||||
<CardTitle className="text-xl">License Key</CardTitle>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{enabled ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
disabled={isLoading || isSaving}
|
||||
onCheckedChange={async (next) => {
|
||||
try {
|
||||
await updateEnterpriseSettings({
|
||||
enableEnterpriseFeatures: next,
|
||||
});
|
||||
await utils.licenseKey.getEnterpriseSettings.invalidate();
|
||||
toast.success("Enterprise features updated");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Failed to update enterprise features");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
To unlock extra features you need an enterprise license key. Contact
|
||||
us{" "}
|
||||
<Link
|
||||
href="http://localhost:3001/contact"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline underline-offset-4"
|
||||
>
|
||||
here
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<div className="grid gap-3 md:grid-cols-[1fr_auto] md:items-end">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor="licenseKey">
|
||||
License Key
|
||||
</label>
|
||||
<Input
|
||||
id="licenseKey"
|
||||
placeholder="Enter your enterprise license key"
|
||||
value={licenseKey}
|
||||
onChange={(e) => setLicenseKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:justify-self-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={isSaving}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await updateEnterpriseSettings({ licenseKey });
|
||||
await utils.licenseKey.getEnterpriseSettings.invalidate();
|
||||
toast.success("License key saved");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Failed to save license key");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Key } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -13,14 +14,27 @@ export function LicenseKeySettings() {
|
||||
const { data, isLoading } = api.licenseKey.getEnterpriseSettings.useQuery();
|
||||
const { mutateAsync: updateEnterpriseSettings, isLoading: isSaving } =
|
||||
api.licenseKey.updateEnterpriseSettings.useMutation();
|
||||
const { mutateAsync: activateLicenseKey, isLoading: isActivating } =
|
||||
api.licenseKey.activate.useMutation();
|
||||
const { mutateAsync: validateLicenseKey, isLoading: isValidating } =
|
||||
api.licenseKey.validate.useMutation();
|
||||
const { mutateAsync: deactivateLicenseKey, isLoading: isDeactivating } =
|
||||
api.licenseKey.deactivate.useMutation();
|
||||
|
||||
const [licenseKey, setLicenseKey] = useState("");
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.licenseKey !== undefined) {
|
||||
setLicenseKey(data.licenseKey ?? "");
|
||||
if (data?.licenseKey) {
|
||||
setLicenseKey(data.licenseKey);
|
||||
validateLicenseKey({ licenseKey: data.licenseKey })
|
||||
.then((valid) => {
|
||||
console.log("valid", valid);
|
||||
setIsValid(valid);
|
||||
})
|
||||
.catch(() => setIsValid(false));
|
||||
}
|
||||
}, [data?.licenseKey]);
|
||||
}, [data?.licenseKey, validateLicenseKey]);
|
||||
|
||||
const enabled = !!data?.enableEnterpriseFeatures;
|
||||
|
||||
@@ -39,7 +53,7 @@ export function LicenseKeySettings() {
|
||||
</span>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
disabled={isLoading || isSaving}
|
||||
disabled={isLoading || isSaving || isDeactivating}
|
||||
onCheckedChange={async (next) => {
|
||||
try {
|
||||
await updateEnterpriseSettings({
|
||||
@@ -57,7 +71,8 @@ export function LicenseKeySettings() {
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
To unlock extra features you need an enterprise license key. Contact us{" "}
|
||||
To unlock extra features you need an enterprise license key. Contact
|
||||
us{" "}
|
||||
<Link
|
||||
href="http://localhost:3001/contact"
|
||||
target="_blank"
|
||||
@@ -83,23 +98,84 @@ export function LicenseKeySettings() {
|
||||
onChange={(e) => setLicenseKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:justify-self-end">
|
||||
<div className="md:justify-self-end flex gap-2">
|
||||
{isValid && (
|
||||
<DialogAction
|
||||
title="Deactivate License Key"
|
||||
description="Are you sure you want to deactivate this license key? This will disable enterprise features."
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deactivateLicenseKey({ licenseKey });
|
||||
await utils.licenseKey.getEnterpriseSettings.invalidate();
|
||||
setIsValid(false);
|
||||
toast.success("License key deactivated");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to deactivate license key",
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={isDeactivating || !data?.licenseKey}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isDeactivating || !data?.licenseKey}
|
||||
>
|
||||
Deactivate
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={isSaving}
|
||||
variant="outline"
|
||||
disabled={isSaving || isValidating || isDeactivating}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await updateEnterpriseSettings({ licenseKey });
|
||||
await utils.licenseKey.getEnterpriseSettings.invalidate();
|
||||
toast.success("License key saved");
|
||||
const valid = await validateLicenseKey({ licenseKey });
|
||||
if (valid) {
|
||||
toast.success("License key is valid");
|
||||
} else {
|
||||
toast.error("License key is invalid");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Failed to save license key");
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to validate license key",
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
Validate
|
||||
</Button>
|
||||
{!isValid && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={isSaving || isValidating || isDeactivating}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await activateLicenseKey({ licenseKey });
|
||||
await utils.licenseKey.getEnterpriseSettings.invalidate();
|
||||
// Re-validate after saving to update the Deactivate button visibility
|
||||
const valid = await validateLicenseKey({ licenseKey });
|
||||
setIsValid(valid);
|
||||
toast.success("License key activated");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to activate license key",
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Activate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,8 +4,103 @@ import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
activateLicenseKey,
|
||||
deactivateLicenseKey,
|
||||
validateLicenseKey,
|
||||
} from "@/server/utils/enterprise";
|
||||
|
||||
export const licenseKeyRouter = createTRPCRouter({
|
||||
activate: adminProcedure
|
||||
.input(z.object({ licenseKey: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const currentUserId = ctx.user.id;
|
||||
const currentUser = await db.query.user.findFirst({
|
||||
where: eq(user.id, currentUserId),
|
||||
});
|
||||
if (!currentUser) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentUser.enableEnterpriseFeatures) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Please activate enterprise features to activate license key",
|
||||
});
|
||||
}
|
||||
|
||||
return await activateLicenseKey(input.licenseKey);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to activate license key",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
validate: adminProcedure
|
||||
.input(z.object({ licenseKey: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const currentUserId = ctx.user.id;
|
||||
const currentUser = await db.query.user.findFirst({
|
||||
where: eq(user.id, currentUserId),
|
||||
});
|
||||
if (!currentUser) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentUser.enableEnterpriseFeatures) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Please activate enterprise features to validate license key",
|
||||
});
|
||||
}
|
||||
return await validateLicenseKey(input.licenseKey);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to validate license key",
|
||||
});
|
||||
}
|
||||
}),
|
||||
deactivate: adminProcedure
|
||||
.input(z.object({ licenseKey: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const isValidLicenseKey = await validateLicenseKey(input.licenseKey);
|
||||
if (!isValidLicenseKey) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "License key is invalid",
|
||||
});
|
||||
}
|
||||
return await deactivateLicenseKey(input.licenseKey);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to deactivate license key",
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEnterpriseSettings: adminProcedure.query(async ({ ctx }) => {
|
||||
const currentUserId = ctx.user.id;
|
||||
const currentUser = await db.query.user.findFirst({
|
||||
@@ -29,31 +124,35 @@ export const licenseKeyRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
enableEnterpriseFeatures: z.boolean().optional(),
|
||||
licenseKey: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const currentUserId = ctx.user.id;
|
||||
try {
|
||||
const currentUserId = ctx.user.id;
|
||||
|
||||
if (
|
||||
input.enableEnterpriseFeatures === undefined &&
|
||||
input.licenseKey === undefined
|
||||
) {
|
||||
if (input.enableEnterpriseFeatures === undefined) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "enableEnterpriseFeatures must be provided",
|
||||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
enableEnterpriseFeatures: input.enableEnterpriseFeatures,
|
||||
})
|
||||
.where(eq(user.id, currentUserId));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
"At least one of enableEnterpriseFeatures or licenseKey must be provided",
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to update enterprise settings",
|
||||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
// enableEnterpriseFeatures: input.enableEnterpriseFeatures ?? false,
|
||||
licenseKey: input.licenseKey ?? "",
|
||||
})
|
||||
.where(eq(user.id, currentUserId));
|
||||
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
||||
76
apps/dokploy/server/utils/enterprise.ts
Normal file
76
apps/dokploy/server/utils/enterprise.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { getPublicIpWithFallback } from "@dokploy/server/index";
|
||||
|
||||
const LICENSE_KEY_URL = process.env.LICENSE_KEY_URL || "http://localhost:4002";
|
||||
|
||||
export const validateLicenseKey = async (licenseKey: string) => {
|
||||
try {
|
||||
const ip = await getPublicIpWithFallback();
|
||||
const result = await fetch(`${LICENSE_KEY_URL}/licenses/validate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ licenseKey, ip }),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const errorData = await result.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Failed to validate license key");
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
console.log("data", data);
|
||||
return data.valid;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const activateLicenseKey = async (licenseKey: string) => {
|
||||
try {
|
||||
const ip = await getPublicIpWithFallback();
|
||||
const result = await fetch(`${LICENSE_KEY_URL}/licenses/activate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ licenseKey, ip }),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const errorData = await result.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Failed to activate license key");
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deactivateLicenseKey = async (licenseKey: string) => {
|
||||
try {
|
||||
const ip = await getPublicIpWithFallback();
|
||||
const result = await fetch(`${LICENSE_KEY_URL}/licenses/deactivate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ licenseKey, ip }),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const errorData = await result.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Failed to deactivate license key");
|
||||
}
|
||||
|
||||
const data = await result.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user