mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 04:35:24 +02:00
Add enterprise features management: implement license key settings and update user schema
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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.organization.getEnterpriseSettings.useQuery();
|
||||
const { mutateAsync: updateEnterpriseSettings, isLoading: isSaving } =
|
||||
api.organization.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.organization.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.organization.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
apps/dokploy/drizzle/0137_naive_power_pack.sql
Normal file
2
apps/dokploy/drizzle/0137_naive_power_pack.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "user" ADD COLUMN "enableEnterpriseFeatures" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "licenseKey" text;
|
||||
7063
apps/dokploy/drizzle/meta/0137_snapshot.json
Normal file
7063
apps/dokploy/drizzle/meta/0137_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -960,6 +960,13 @@
|
||||
"when": 1769580434296,
|
||||
"tag": "0136_tidy_puff_adder",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 137,
|
||||
"version": "7",
|
||||
"when": 1769616589728,
|
||||
"tag": "0137_naive_power_pack",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import superjson from "superjson";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||
import { LicenseKeySettings } from "@/components/dashboard/settings/web-server/license-key";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
@@ -28,6 +29,13 @@ const Page = () => {
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<div className="p-6">
|
||||
<LicenseKeySettings />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,51 @@ import { and, desc, eq, exists } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/server/db";
|
||||
import { invitation, member, organization } from "@/server/db/schema";
|
||||
import { invitation, member, organization, user } from "@/server/db/schema";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
export const organizationRouter = createTRPCRouter({
|
||||
getEnterpriseSettings: adminProcedure.query(async ({ ctx }) => {
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
enableEnterpriseFeatures: !!currentUser.enableEnterpriseFeatures,
|
||||
licenseKey: currentUser.licenseKey ?? "",
|
||||
};
|
||||
}),
|
||||
|
||||
updateEnterpriseSettings: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
enableEnterpriseFeatures: z.boolean().optional(),
|
||||
licenseKey: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const currentUserId = ctx.user.id;
|
||||
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
...(input.enableEnterpriseFeatures === undefined
|
||||
? {}
|
||||
: { enableEnterpriseFeatures: input.enableEnterpriseFeatures }),
|
||||
...(input.licenseKey === undefined ? {} : { licenseKey: input.licenseKey }),
|
||||
})
|
||||
.where(eq(user.id, currentUserId));
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -53,6 +53,11 @@ export const user = pgTable("user", {
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
||||
// Enterprise / proprietary features
|
||||
enableEnterpriseFeatures: boolean("enableEnterpriseFeatures")
|
||||
.notNull()
|
||||
.default(false),
|
||||
licenseKey: text("licenseKey"),
|
||||
stripeCustomerId: text("stripeCustomerId"),
|
||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||
|
||||
Reference in New Issue
Block a user