refactor(dokploy): restrict license key access to owners only and enhance validation

- Updated the license key settings to ensure only users with the "owner" role can access certain functionalities.
- Modified the license key activation input validation to require a non-empty string.
- Improved error handling for network issues when validating license keys, providing clearer feedback to users.
- Adjusted the dashboard settings to redirect non-owner users appropriately.
This commit is contained in:
Mauricio Siu
2026-02-09 01:15:35 -06:00
parent 0a3a90c4e9
commit f5fa39b97e
5 changed files with 53 additions and 6 deletions

View File

@@ -404,8 +404,7 @@ const MENU: Menu = {
url: "/dashboard/settings/license", url: "/dashboard/settings/license",
icon: Key, icon: Key,
// Only enabled for admins in non-cloud environments // Only enabled for admins in non-cloud environments
isEnabled: ({ auth }) => isEnabled: ({ auth }) => !!(auth?.role === "owner"),
!!(auth?.role === "owner" || auth?.role === "admin"),
}, },
{ {
isSingle: true, isSingle: true,

View File

@@ -166,7 +166,12 @@ export function LicenseKeySettings() {
{!haveValidLicenseKey && ( {!haveValidLicenseKey && (
<Button <Button
variant="secondary" variant="secondary"
disabled={isSaving || isValidating || isDeactivating} disabled={
isSaving ||
isValidating ||
isDeactivating ||
!licenseKey.trim()
}
isLoading={isActivating} isLoading={isActivating}
onClick={async () => { onClick={async () => {
try { try {

View File

@@ -1,4 +1,4 @@
import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
@@ -45,7 +45,7 @@ export async function getServerSideProps(
}, },
}; };
} }
if (user.role === "member") { if (user.role !== "owner") {
return { return {
redirect: { redirect: {
permanent: true, permanent: true,

View File

@@ -12,7 +12,7 @@ import {
export const licenseKeyRouter = createTRPCRouter({ export const licenseKeyRouter = createTRPCRouter({
activate: adminProcedure activate: adminProcedure
.input(z.object({ licenseKey: z.string() })) .input(z.object({ licenseKey: z.string().min(1) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
const currentUserId = ctx.user.id; const currentUserId = ctx.user.id;
@@ -74,6 +74,13 @@ export const licenseKeyRouter = createTRPCRouter({
}); });
} }
if (ctx.user.role !== "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not authorized to validate a license key",
});
}
if (!currentUser.licenseKey) { if (!currentUser.licenseKey) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
@@ -164,6 +171,13 @@ export const licenseKeyRouter = createTRPCRouter({
}); });
} }
if (ctx.user.role !== "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not authorized to get enterprise settings",
});
}
return { return {
enableEnterpriseFeatures: !!currentUser.enableEnterpriseFeatures, enableEnterpriseFeatures: !!currentUser.enableEnterpriseFeatures,
licenseKey: currentUser.licenseKey ?? "", licenseKey: currentUser.licenseKey ?? "",
@@ -200,6 +214,13 @@ export const licenseKeyRouter = createTRPCRouter({
}); });
} }
if (ctx.user.role !== "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not authorized to update enterprise settings",
});
}
await db await db
.update(user) .update(user)
.set({ .set({

View File

@@ -1,5 +1,18 @@
import { getPublicIpWithFallback, LICENSE_KEY_URL } from "@dokploy/server"; import { getPublicIpWithFallback, LICENSE_KEY_URL } from "@dokploy/server";
const LICENSE_SERVER_UNREACHABLE =
"Could not reach the license server. Check your connection or try again later.";
function isNetworkError(error: unknown): boolean {
if (error instanceof Error) {
if (error.message === "fetch failed") return true;
const cause = (error as Error & { cause?: { code?: string } }).cause;
const code = cause?.code;
return code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT";
}
return false;
}
export const validateLicenseKey = async (licenseKey: string) => { export const validateLicenseKey = async (licenseKey: string) => {
try { try {
const ip = await getPublicIpWithFallback(); const ip = await getPublicIpWithFallback();
@@ -22,6 +35,9 @@ export const validateLicenseKey = async (licenseKey: string) => {
console.error( console.error(
error instanceof Error ? error.message : "Failed to validate license key", error instanceof Error ? error.message : "Failed to validate license key",
); );
if (isNetworkError(error)) {
throw new Error(LICENSE_SERVER_UNREACHABLE);
}
throw error; throw error;
} }
}; };
@@ -48,6 +64,9 @@ export const activateLicenseKey = async (licenseKey: string) => {
console.error( console.error(
error instanceof Error ? error.message : "Failed to activate license key", error instanceof Error ? error.message : "Failed to activate license key",
); );
if (isNetworkError(error)) {
throw new Error(LICENSE_SERVER_UNREACHABLE);
}
throw error; throw error;
} }
}; };
@@ -76,6 +95,9 @@ export const deactivateLicenseKey = async (licenseKey: string) => {
? error.message ? error.message
: "Failed to deactivate license key", : "Failed to deactivate license key",
); );
if (isNetworkError(error)) {
throw new Error(LICENSE_SERVER_UNREACHABLE);
}
throw error; throw error;
} }
}; };