diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 2e5309370..1be4eaaca 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -214,6 +214,13 @@ if (build === "saas") { generateLicense.generateNewEnterpriseLicense ); + authenticated.post( + "/org/:orgId/license/:licenseKey/clear-instance-name", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.billing), + generateLicense.clearInstanceName + ); + authenticated.post( "/send-support-request", rateLimit({ diff --git a/server/private/routers/generatedLicense/clearInstanceName.ts b/server/private/routers/generatedLicense/clearInstanceName.ts new file mode 100644 index 000000000..ed176a976 --- /dev/null +++ b/server/private/routers/generatedLicense/clearInstanceName.ts @@ -0,0 +1,87 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { response as sendResponse } from "@server/lib/response"; +import privateConfig from "#private/lib/config"; +import z from "zod"; +import { fromError } from "zod-validation-error"; + +const clearInstanceNameParamsSchema = z.object({ + orgId: z.string(), + licenseKey: z.string() +}); + +export async function clearInstanceName( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = clearInstanceNameParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { licenseKey } = parsedParams.data; + + const apiResponse = await fetch( + `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/clear-instance-name`, + { + method: "POST", + headers: { + "api-key": + privateConfig.getRawPrivateConfig().server + .fossorial_api_key!, + "Content-Type": "application/json" + }, + body: JSON.stringify({ licenseKey }) + } + ); + + const data = await apiResponse.json(); + + if (!data.success || data.error) { + return next( + createHttpError( + data.status || HttpCode.BAD_REQUEST, + data.message || "Failed to clear instance name from Fossorial API" + ) + ); + } + + return sendResponse(res, { + data: null, + success: true, + error: false, + message: "Instance name cleared successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred while clearing the instance name." + ) + ); + } +} \ No newline at end of file diff --git a/server/private/routers/generatedLicense/index.ts b/server/private/routers/generatedLicense/index.ts index e9212b47e..b527dc721 100644 --- a/server/private/routers/generatedLicense/index.ts +++ b/server/private/routers/generatedLicense/index.ts @@ -14,3 +14,4 @@ export * from "./listGeneratedLicenses"; export * from "./generateNewLicense"; export * from "./generateNewEnterpriseLicense"; +export * from "./clearInstanceName"; diff --git a/src/components/GenerateLicenseKeysTable.tsx b/src/components/GenerateLicenseKeysTable.tsx index 48eeb045e..e6961251b 100644 --- a/src/components/GenerateLicenseKeysTable.tsx +++ b/src/components/GenerateLicenseKeysTable.tsx @@ -4,7 +4,7 @@ import { useTranslations } from "next-intl"; import { ColumnDef } from "@tanstack/react-table"; import { ExtendedColumnDef } from "@app/components/ui/data-table"; import { Button } from "./ui/button"; -import { ArrowUpDown } from "lucide-react"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; import CopyToClipboard from "./CopyToClipboard"; import { Badge } from "./ui/badge"; import moment from "moment"; @@ -16,6 +16,12 @@ import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import NewPricingLicenseForm from "./NewPricingLicenseForm"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; type GnerateLicenseKeysTableProps = { licenseKeys: GeneratedLicenseKey[]; @@ -44,6 +50,7 @@ export default function GenerateLicenseKeysTable({ const [isRefreshing, setIsRefreshing] = useState(false); const [showGenerateForm, setShowGenerateForm] = useState(false); + const [isClearingInstanceName, setIsClearingInstanceName] = useState(false); useEffect(() => { if (searchParams.get(GENERATE_QUERY) !== null) { @@ -63,6 +70,28 @@ export default function GenerateLicenseKeysTable({ refreshData(); }; + const clearInstanceName = async (licenseKey: string) => { + setIsClearingInstanceName(true); + try { + await api.post( + `/org/${orgId}/license/${encodeURIComponent(licenseKey)}/clear-instance-name` + ); + toast({ + title: t("success"), + description: "Instance name cleared successfully" + }); + await refreshData(); + } catch (error) { + toast({ + title: t("error"), + description: formatAxiosError(error, "Failed to clear instance name"), + variant: "destructive" + }); + } finally { + setIsClearingInstanceName(false); + } + }; + const refreshData = async () => { console.log("Data refreshed"); setIsRefreshing(true); @@ -236,6 +265,39 @@ export default function GenerateLicenseKeysTable({ const termianteAt = row.original.expiresAt; return moment(termianteAt).format("lll"); } + }, + { + id: "actions", + enableHiding: false, + header: () => , + cell: ({ row }) => { + const key = row.original; + return ( +
+ + + + + + + clearInstanceName(key.licenseKey) + } + > + Clear Instance Name + + + +
+ ); + } } ]; @@ -254,6 +316,7 @@ export default function GenerateLicenseKeysTable({ onAdd={() => { setShowGenerateForm(true); }} + stickyRightColumn="actions" />