mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 04:35:24 +02:00
Enhance License Key Management and Enterprise Features: Update license key validation logic to ensure proper handling of enterprise licenses, including new cron job for refreshing license validity. Introduce new SQL migration for isValidEnterpriseLicense column and refactor related API procedures for better error handling and user feedback.
This commit is contained in:
@@ -113,6 +113,7 @@ export function LicenseKeySettings() {
|
||||
await deactivateLicenseKey();
|
||||
await utils.licenseKey.getEnterpriseSettings.invalidate();
|
||||
await utils.licenseKey.haveValidLicenseKey.invalidate();
|
||||
setLicenseKey("");
|
||||
toast.success("License key deactivated");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -143,10 +144,7 @@ export function LicenseKeySettings() {
|
||||
isLoading={isValidating}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const valid = await validateLicenseKey({
|
||||
licenseKey,
|
||||
});
|
||||
console.log("valid", valid);
|
||||
const valid = await validateLicenseKey();
|
||||
if (valid) {
|
||||
toast.success("License key is valid");
|
||||
} else {
|
||||
|
||||
1
apps/dokploy/drizzle/0139_smiling_havok.sql
Normal file
1
apps/dokploy/drizzle/0139_smiling_havok.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "isValidEnterpriseLicense" boolean DEFAULT false NOT NULL;
|
||||
7153
apps/dokploy/drizzle/meta/0139_snapshot.json
Normal file
7153
apps/dokploy/drizzle/meta/0139_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -974,6 +974,13 @@
|
||||
"when": 1769745328628,
|
||||
"tag": "0138_common_mathemanic",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 139,
|
||||
"version": "7",
|
||||
"when": 1769746948088,
|
||||
"tag": "0139_smiling_havok",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
47
apps/dokploy/pages/api/cron/refresh-license-validity.ts
Normal file
47
apps/dokploy/pages/api/cron/refresh-license-validity.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { refreshAllLicenseValidity } from "@/server/utils/enterprise";
|
||||
|
||||
/**
|
||||
* Cron endpoint to refresh isValidEnterpriseLicense for all users with a license key.
|
||||
* Call every 2 weeks (e.g. 0 0 1,15 * * for 1st and 15th, or via your hosting cron).
|
||||
*
|
||||
* Requires CRON_SECRET in Authorization header or query: ?secret=CRON_SECRET
|
||||
*/
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (req.method !== "GET" && req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const secret =
|
||||
process.env.CRON_SECRET ?? process.env.LICENSE_CRON_SECRET;
|
||||
if (!secret) {
|
||||
return res.status(500).json({
|
||||
error: "CRON_SECRET or LICENSE_CRON_SECRET not configured",
|
||||
});
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
const bearer = authHeader?.startsWith("Bearer ")
|
||||
? authHeader.slice(7)
|
||||
: undefined;
|
||||
const querySecret = typeof req.query.secret === "string" ? req.query.secret : undefined;
|
||||
|
||||
if (bearer !== secret && querySecret !== secret) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await refreshAllLicenseValidity();
|
||||
return res.status(200).json(result);
|
||||
} catch (err) {
|
||||
console.error("refresh-license-validity:", err);
|
||||
return res
|
||||
.status(500)
|
||||
.json({
|
||||
error: err instanceof Error ? err.message : "Refresh failed",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { user } from "@dokploy/server/db/schema";
|
||||
import { validateLicenseKey } from "@dokploy/server/index";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
@@ -7,7 +8,6 @@ import { db } from "@/server/db";
|
||||
import {
|
||||
activateLicenseKey,
|
||||
deactivateLicenseKey,
|
||||
validateLicenseKey,
|
||||
} from "@/server/utils/enterprise";
|
||||
|
||||
export const licenseKeyRouter = createTRPCRouter({
|
||||
@@ -34,7 +34,15 @@ export const licenseKeyRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
return await activateLicenseKey(input.licenseKey);
|
||||
await activateLicenseKey(input.licenseKey);
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
licenseKey: input.licenseKey,
|
||||
isValidEnterpriseLicense: true,
|
||||
})
|
||||
.where(eq(user.id, currentUserId));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
@@ -46,39 +54,51 @@ export const licenseKeyRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
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) {
|
||||
validate: adminProcedure.mutation(async ({ 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: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to validate license key",
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
if (!currentUser.licenseKey) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "No license key found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentUser.enableEnterpriseFeatures) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Please activate enterprise features to validate license key",
|
||||
});
|
||||
}
|
||||
const valid = await validateLicenseKey(currentUser.licenseKey);
|
||||
if (valid) {
|
||||
await db
|
||||
.update(user)
|
||||
.set({ isValidEnterpriseLicense: true })
|
||||
.where(eq(user.id, currentUserId));
|
||||
}
|
||||
return valid;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to validate license key",
|
||||
});
|
||||
}
|
||||
}),
|
||||
deactivate: adminProcedure.mutation(async ({ ctx }) => {
|
||||
try {
|
||||
const currentUserId = ctx.user.id;
|
||||
@@ -97,7 +117,15 @@ export const licenseKeyRouter = createTRPCRouter({
|
||||
message: "No license key found",
|
||||
});
|
||||
}
|
||||
return await deactivateLicenseKey(currentUser.licenseKey);
|
||||
await deactivateLicenseKey(currentUser.licenseKey);
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
licenseKey: null,
|
||||
isValidEnterpriseLicense: false,
|
||||
})
|
||||
.where(eq(user.id, currentUserId));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
@@ -130,18 +158,15 @@ export const licenseKeyRouter = createTRPCRouter({
|
||||
const currentUserId = ctx.user.id;
|
||||
const currentUser = await db.query.user.findFirst({
|
||||
where: eq(user.id, currentUserId),
|
||||
columns: {
|
||||
enableEnterpriseFeatures: true,
|
||||
isValidEnterpriseLicense: true,
|
||||
},
|
||||
});
|
||||
if (!currentUser?.enableEnterpriseFeatures) {
|
||||
return false;
|
||||
}
|
||||
if (!currentUser.licenseKey) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await validateLicenseKey(currentUser.licenseKey ?? "");
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return !!(
|
||||
currentUser?.enableEnterpriseFeatures &&
|
||||
currentUser?.isValidEnterpriseLicense
|
||||
);
|
||||
}),
|
||||
|
||||
updateEnterpriseSettings: adminProcedure
|
||||
|
||||
@@ -2,11 +2,11 @@ import { ssoProvider } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
|
||||
import { createTRPCRouter, enterpriseProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
|
||||
export const ssoRouter = createTRPCRouter({
|
||||
listProviders: adminProcedure.query(async ({ ctx }) => {
|
||||
listProviders: enterpriseProcedure.query(async ({ ctx }) => {
|
||||
const providers = await db.query.ssoProvider.findMany({
|
||||
where: eq(ssoProvider.userId, ctx.user.id),
|
||||
columns: {
|
||||
@@ -22,7 +22,7 @@ export const ssoRouter = createTRPCRouter({
|
||||
return providers;
|
||||
}),
|
||||
|
||||
deleteProvider: adminProcedure
|
||||
deleteProvider: enterpriseProcedure
|
||||
.input(z.object({ providerId: z.string().min(1) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const [deleted] = await db
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
* need to use are documented accordingly near the end.
|
||||
*/
|
||||
|
||||
import { user as userSchema } from "@dokploy/server/db/schema";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import {
|
||||
@@ -217,3 +219,40 @@ export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Requires admin/owner role AND enterprise enabled with a license key in DB.
|
||||
* Does NOT call the license server on every request; full validation (haveValidLicenseKey)
|
||||
* is used in the UI gate and when activating/validating keys.
|
||||
*/
|
||||
export const enterpriseProcedure = t.procedure.use(async ({ ctx, next }) => {
|
||||
if (
|
||||
!ctx.session ||
|
||||
!ctx.user ||
|
||||
(ctx.user.role !== "owner" && ctx.user.role !== "admin")
|
||||
) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const currentUser = await ctx.db.query.user.findFirst({
|
||||
where: eq(userSchema.id, ctx.user.id),
|
||||
columns: {
|
||||
enableEnterpriseFeatures: true,
|
||||
isValidEnterpriseLicense: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!currentUser?.enableEnterpriseFeatures || !currentUser.isValidEnterpriseLicense) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Valid enterprise license required",
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
session: ctx.session,
|
||||
user: ctx.user,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,9 @@ export const user = pgTable("user", {
|
||||
.notNull()
|
||||
.default(false),
|
||||
licenseKey: text("licenseKey"),
|
||||
isValidEnterpriseLicense: boolean("isValidEnterpriseLicense")
|
||||
.notNull()
|
||||
.default(false),
|
||||
stripeCustomerId: text("stripeCustomerId"),
|
||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||
|
||||
@@ -79,6 +79,7 @@ export * from "./utils/builders/paketo";
|
||||
export * from "./utils/builders/static";
|
||||
export * from "./utils/builders/utils";
|
||||
export * from "./utils/cluster/upload";
|
||||
export * from "./utils/crons/enterprise";
|
||||
export * from "./utils/databases/rebuild";
|
||||
export * from "./utils/docker/collision";
|
||||
export * from "./utils/docker/compose";
|
||||
|
||||
70
packages/server/src/utils/crons/enterprise.ts
Normal file
70
packages/server/src/utils/crons/enterprise.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { member } from "@dokploy/server/db/schema";
|
||||
import { getPublicIpWithFallback } from "@dokploy/server/wss/utils";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { scheduleJob } from "node-schedule";
|
||||
import { db } from "../../db/index";
|
||||
import { user as userSchema } from "../../db/schema/user";
|
||||
|
||||
export const initEnterpriseBackupCronJobs = async () => {
|
||||
console.log("Setting up enterprise backup cron jobs....");
|
||||
|
||||
const admins = await db.query.member.findMany({
|
||||
where: eq(member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const admin of admins) {
|
||||
const { user } = admin;
|
||||
if (user.isValidEnterpriseLicense) {
|
||||
scheduleJob(`enterprise-backup-${user.id}`, "0 0 */14 * *", async () => {
|
||||
try {
|
||||
console.log(
|
||||
"Validating license key....",
|
||||
user.firstName,
|
||||
user.lastName,
|
||||
);
|
||||
const isValid = await validateLicenseKey(user.licenseKey || "");
|
||||
if (!isValid) {
|
||||
throw new Error("License key is invalid");
|
||||
}
|
||||
} catch (error) {
|
||||
await db
|
||||
.update(userSchema)
|
||||
.set({ isValidEnterpriseLicense: false })
|
||||
.where(eq(userSchema.id, user.id));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const validateLicenseKey = async (licenseKey: string) => {
|
||||
try {
|
||||
const ip = await getPublicIpWithFallback();
|
||||
const result = await fetch(
|
||||
`${process.env.LICENSE_KEY_URL || "http://localhost:4002"}/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();
|
||||
return data.valid;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
error instanceof Error ? error.message : "Failed to validate license key",
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user