Allow clearing the instance name on the licenses

This commit is contained in:
Owen
2026-04-16 14:40:44 -07:00
parent a246de2b1f
commit 6fe74a9f8d
4 changed files with 159 additions and 1 deletions

View File

@@ -214,6 +214,13 @@ if (build === "saas") {
generateLicense.generateNewEnterpriseLicense generateLicense.generateNewEnterpriseLicense
); );
authenticated.post(
"/org/:orgId/license/:licenseKey/clear-instance-name",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.billing),
generateLicense.clearInstanceName
);
authenticated.post( authenticated.post(
"/send-support-request", "/send-support-request",
rateLimit({ rateLimit({

View File

@@ -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<any> {
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<null>(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."
)
);
}
}

View File

@@ -14,3 +14,4 @@
export * from "./listGeneratedLicenses"; export * from "./listGeneratedLicenses";
export * from "./generateNewLicense"; export * from "./generateNewLicense";
export * from "./generateNewEnterpriseLicense"; export * from "./generateNewEnterpriseLicense";
export * from "./clearInstanceName";

View File

@@ -4,7 +4,7 @@ import { useTranslations } from "next-intl";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { ExtendedColumnDef } from "@app/components/ui/data-table"; import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { ArrowUpDown } from "lucide-react"; import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import CopyToClipboard from "./CopyToClipboard"; import CopyToClipboard from "./CopyToClipboard";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import moment from "moment"; import moment from "moment";
@@ -16,6 +16,12 @@ import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import NewPricingLicenseForm from "./NewPricingLicenseForm"; import NewPricingLicenseForm from "./NewPricingLicenseForm";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu";
type GnerateLicenseKeysTableProps = { type GnerateLicenseKeysTableProps = {
licenseKeys: GeneratedLicenseKey[]; licenseKeys: GeneratedLicenseKey[];
@@ -44,6 +50,7 @@ export default function GenerateLicenseKeysTable({
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [showGenerateForm, setShowGenerateForm] = useState(false); const [showGenerateForm, setShowGenerateForm] = useState(false);
const [isClearingInstanceName, setIsClearingInstanceName] = useState(false);
useEffect(() => { useEffect(() => {
if (searchParams.get(GENERATE_QUERY) !== null) { if (searchParams.get(GENERATE_QUERY) !== null) {
@@ -63,6 +70,28 @@ export default function GenerateLicenseKeysTable({
refreshData(); 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 () => { const refreshData = async () => {
console.log("Data refreshed"); console.log("Data refreshed");
setIsRefreshing(true); setIsRefreshing(true);
@@ -236,6 +265,39 @@ export default function GenerateLicenseKeysTable({
const termianteAt = row.original.expiresAt; const termianteAt = row.original.expiresAt;
return moment(termianteAt).format("lll"); return moment(termianteAt).format("lll");
} }
},
{
id: "actions",
enableHiding: false,
header: () => <span className="p-3"></span>,
cell: ({ row }) => {
const key = row.original;
return (
<div className="flex items-center gap-2 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={
!key.instanceName ||
isClearingInstanceName
}
onClick={() =>
clearInstanceName(key.licenseKey)
}
>
Clear Instance Name
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
} }
]; ];
@@ -254,6 +316,7 @@ export default function GenerateLicenseKeysTable({
onAdd={() => { onAdd={() => {
setShowGenerateForm(true); setShowGenerateForm(true);
}} }}
stickyRightColumn="actions"
/> />
<NewPricingLicenseForm <NewPricingLicenseForm