mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-17 18:36:37 +00:00
add license system and ui
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
||||
} from "@app/components/InfoSection";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
|
||||
@@ -67,6 +68,7 @@ export default function GeneralPage() {
|
||||
const { idpId } = useParams();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
||||
|
||||
@@ -230,7 +232,7 @@ export default function GeneralPage() {
|
||||
defaultChecked={form.getValues(
|
||||
"autoProvision"
|
||||
)}
|
||||
disabled={true}
|
||||
disabled={!isUnlocked()}
|
||||
onCheckedChange={(checked) => {
|
||||
form.setValue(
|
||||
"autoProvision",
|
||||
@@ -238,12 +240,14 @@ export default function GeneralPage() {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Badge
|
||||
variant="outlinePrimary"
|
||||
className="ml-2"
|
||||
>
|
||||
Professional
|
||||
</Badge>
|
||||
{!isUnlocked() && (
|
||||
<Badge
|
||||
variant="outlinePrimary"
|
||||
className="ml-2"
|
||||
>
|
||||
Professional
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
When enabled, users will be
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AxiosResponse } from "axios";
|
||||
import { redirect } from "next/navigation";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { ProfessionalContentOverlay } from "@app/components/ProfessionalContentOverlay";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
@@ -35,7 +36,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
redirect("/admin/idp");
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
const navItems: HorizontalTabs = [
|
||||
{
|
||||
title: "General",
|
||||
href: `/admin/idp/${params.idpId}/general`
|
||||
|
||||
@@ -36,6 +36,7 @@ import { InfoIcon, ExternalLink } from "lucide-react";
|
||||
import { StrategySelect } from "@app/components/StrategySelect";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
|
||||
const createIdpFormSchema = z.object({
|
||||
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
|
||||
@@ -74,6 +75,7 @@ export default function Page() {
|
||||
const api = createApiClient({ env });
|
||||
const router = useRouter();
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const form = useForm<CreateIdpFormValues>({
|
||||
resolver: zodResolver(createIdpFormSchema),
|
||||
@@ -190,7 +192,7 @@ export default function Page() {
|
||||
defaultChecked={form.getValues(
|
||||
"autoProvision"
|
||||
)}
|
||||
disabled={true}
|
||||
disabled={!isUnlocked()}
|
||||
onCheckedChange={(checked) => {
|
||||
form.setValue(
|
||||
"autoProvision",
|
||||
@@ -198,12 +200,14 @@ export default function Page() {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Badge
|
||||
variant="outlinePrimary"
|
||||
className="ml-2"
|
||||
>
|
||||
Professional
|
||||
</Badge>
|
||||
{!isUnlocked() && (
|
||||
<Badge
|
||||
variant="outlinePrimary"
|
||||
className="ml-2"
|
||||
>
|
||||
Professional
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
When enabled, users will be
|
||||
|
||||
147
src/app/admin/license/LicenseKeysDataTable.tsx
Normal file
147
src/app/admin/license/LicenseKeysDataTable.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { LicenseKeyCache } from "@server/license/license";
|
||||
import { ArrowUpDown } from "lucide-react";
|
||||
import moment from "moment";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
|
||||
type LicenseKeysDataTableProps = {
|
||||
licenseKeys: LicenseKeyCache[];
|
||||
onDelete: (key: string) => void;
|
||||
onCreate: () => void;
|
||||
};
|
||||
|
||||
function obfuscateLicenseKey(key: string): string {
|
||||
if (key.length <= 8) return key;
|
||||
const firstPart = key.substring(0, 4);
|
||||
const lastPart = key.substring(key.length - 4);
|
||||
return `${firstPart}••••••••••••••••••••${lastPart}`;
|
||||
}
|
||||
|
||||
export function LicenseKeysDataTable({
|
||||
licenseKeys,
|
||||
onDelete,
|
||||
onCreate
|
||||
}: LicenseKeysDataTableProps) {
|
||||
const columns: ColumnDef<LicenseKeyCache>[] = [
|
||||
{
|
||||
accessorKey: "licenseKey",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
License Key
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const licenseKey = row.original.licenseKey;
|
||||
return (
|
||||
<CopyToClipboard
|
||||
text={licenseKey}
|
||||
displayText={obfuscateLicenseKey(licenseKey)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "valid",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Valid
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return row.original.valid ? "Yes" : "No";
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Type
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const type = row.original.type;
|
||||
const label =
|
||||
type === "SITES" ? "Additional Sites" : "Host License";
|
||||
const variant = type === "SITES" ? "secondary" : "default";
|
||||
return row.original.valid ? (
|
||||
<Badge variant={variant}>{label}</Badge>
|
||||
) : null;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "numSites",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Number of Sites
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button
|
||||
variant="outlinePrimary"
|
||||
onClick={() => onDelete(row.original.licenseKey)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={licenseKeys}
|
||||
title="License Keys"
|
||||
searchPlaceholder="Search license keys..."
|
||||
searchColumn="licenseKey"
|
||||
onAdd={onCreate}
|
||||
addButtonText="Add License Key"
|
||||
/>
|
||||
);
|
||||
}
|
||||
131
src/app/admin/license/components/SitePriceCalculator.tsx
Normal file
131
src/app/admin/license/components/SitePriceCalculator.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { MinusCircle, PlusCircle } from "lucide-react";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
|
||||
type SitePriceCalculatorProps = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
mode: "license" | "additional-sites";
|
||||
};
|
||||
|
||||
export function SitePriceCalculator({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
mode
|
||||
}: SitePriceCalculatorProps) {
|
||||
const [siteCount, setSiteCount] = useState(1);
|
||||
const pricePerSite = 5;
|
||||
const licenseFlatRate = 125;
|
||||
|
||||
const incrementSites = () => {
|
||||
setSiteCount((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const decrementSites = () => {
|
||||
setSiteCount((prev) => (prev > 1 ? prev - 1 : 1));
|
||||
};
|
||||
|
||||
const totalCost = mode === "license"
|
||||
? licenseFlatRate + (siteCount * pricePerSite)
|
||||
: siteCount * pricePerSite;
|
||||
|
||||
return (
|
||||
<Credenza open={isOpen} onOpenChange={onOpenChange}>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{mode === "license" ? "Purchase License" : "Purchase Additional Sites"}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Choose how many sites you want to {mode === "license" ? "purchase a license for" : "add to your existing license"}.
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
Number of Sites
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={decrementSites}
|
||||
disabled={siteCount <= 1}
|
||||
aria-label="Decrease site count"
|
||||
>
|
||||
<MinusCircle className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="text-3xl w-12 text-center">
|
||||
{siteCount}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={incrementSites}
|
||||
aria-label="Increase site count"
|
||||
>
|
||||
<PlusCircle className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
{mode === "license" && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
License fee:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
${licenseFlatRate.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-sm font-medium">
|
||||
Price per site:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
${pricePerSite.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-sm font-medium">
|
||||
Number of sites:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{siteCount}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-4 text-lg font-bold">
|
||||
<span>Total:</span>
|
||||
<span>${totalCost.toFixed(2)} / mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</CredenzaClose>
|
||||
<Button>Continue to Payment</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
471
src/app/admin/license/page.tsx
Normal file
471
src/app/admin/license/page.tsx
Normal file
@@ -0,0 +1,471 @@
|
||||
// This file is licensed under the Fossorial Commercial License.
|
||||
// Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
//
|
||||
// Copyright (c) 2025 Fossorial LLC. All rights reserved.
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { LicenseKeyCache } from "@server/license/license";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { LicenseKeysDataTable } from "./LicenseKeysDataTable";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSectionTitle as SSTitle,
|
||||
SettingsSection,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionGrid,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionFooter
|
||||
} from "@app/components/Settings";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { Check, ShieldCheck, ShieldOff } from "lucide-react";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import { Progress } from "@app/components/ui/progress";
|
||||
import { MinusCircle, PlusCircle } from "lucide-react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { SitePriceCalculator } from "./components/SitePriceCalculator";
|
||||
|
||||
const formSchema = z.object({
|
||||
licenseKey: z
|
||||
.string()
|
||||
.nonempty({ message: "License key is required" })
|
||||
.max(255)
|
||||
});
|
||||
|
||||
function obfuscateLicenseKey(key: string): string {
|
||||
if (key.length <= 8) return key;
|
||||
const firstPart = key.substring(0, 4);
|
||||
const lastPart = key.substring(key.length - 4);
|
||||
return `${firstPart}••••••••••••••••••••${lastPart}`;
|
||||
}
|
||||
|
||||
export default function LicensePage() {
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [rows, setRows] = useState<LicenseKeyCache[]>([]);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedLicenseKey, setSelectedLicenseKey] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const router = useRouter();
|
||||
const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext();
|
||||
const [hostLicense, setHostLicense] = useState<string | null>(null);
|
||||
const [isPurchaseModalOpen, setIsPurchaseModalOpen] = useState(false);
|
||||
const [purchaseMode, setPurchaseMode] = useState<
|
||||
"license" | "additional-sites"
|
||||
>("license");
|
||||
|
||||
// Separate loading states for different actions
|
||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||
const [isActivatingLicense, setIsActivatingLicense] = useState(false);
|
||||
const [isDeletingLicense, setIsDeletingLicense] = useState(false);
|
||||
const [isRecheckingLicense, setIsRecheckingLicense] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
licenseKey: ""
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
setIsInitialLoading(true);
|
||||
await loadLicenseKeys();
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
load();
|
||||
}, []);
|
||||
|
||||
async function loadLicenseKeys() {
|
||||
try {
|
||||
const response =
|
||||
await api.get<AxiosResponse<LicenseKeyCache[]>>(
|
||||
"/license/keys"
|
||||
);
|
||||
const keys = response.data.data;
|
||||
setRows(keys);
|
||||
const hostKey = keys.find((key) => key.type === "LICENSE");
|
||||
if (hostKey) {
|
||||
setHostLicense(hostKey.licenseKey);
|
||||
} else {
|
||||
setHostLicense(null);
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "Failed to load license keys",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred loading license keys"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLicenseKey(key: string) {
|
||||
try {
|
||||
setIsDeletingLicense(true);
|
||||
const res = await api.delete(`/license/${key}`);
|
||||
if (res.data.data) {
|
||||
updateLicenseStatus(res.data.data);
|
||||
}
|
||||
await loadLicenseKeys();
|
||||
toast({
|
||||
title: "License key deleted",
|
||||
description: "The license key has been deleted"
|
||||
});
|
||||
setIsDeleteModalOpen(false);
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "Failed to delete license key",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred deleting license key"
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
setIsDeletingLicense(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function recheck() {
|
||||
try {
|
||||
setIsRecheckingLicense(true);
|
||||
const res = await api.post(`/license/recheck`);
|
||||
if (res.data.data) {
|
||||
updateLicenseStatus(res.data.data);
|
||||
}
|
||||
await loadLicenseKeys();
|
||||
toast({
|
||||
title: "License keys rechecked",
|
||||
description: "All license keys have been rechecked"
|
||||
});
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "Failed to recheck license keys",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred rechecking license keys"
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
setIsRecheckingLicense(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
try {
|
||||
setIsActivatingLicense(true);
|
||||
const res = await api.post("/license/activate", {
|
||||
licenseKey: values.licenseKey
|
||||
});
|
||||
if (res.data.data) {
|
||||
updateLicenseStatus(res.data.data);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "License key activated",
|
||||
description: "The license key has been successfully activated."
|
||||
});
|
||||
|
||||
setIsCreateModalOpen(false);
|
||||
form.reset();
|
||||
await loadLicenseKeys();
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to activate license key",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while activating the license key."
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
setIsActivatingLicense(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isInitialLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SitePriceCalculator
|
||||
isOpen={isPurchaseModalOpen}
|
||||
onOpenChange={(val) => {
|
||||
setIsPurchaseModalOpen(val);
|
||||
}}
|
||||
mode={purchaseMode}
|
||||
/>
|
||||
|
||||
<Credenza
|
||||
open={isCreateModalOpen}
|
||||
onOpenChange={(val) => {
|
||||
setIsCreateModalOpen(val);
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Activate License Key</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Enter a license key to activate it.
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="activate-license-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="licenseKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>License Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
form="activate-license-form"
|
||||
loading={isActivatingLicense}
|
||||
disabled={isActivatingLicense}
|
||||
>
|
||||
Activate License
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
{selectedLicenseKey && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelectedLicenseKey(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
Are you sure you want to delete the license key{" "}
|
||||
<b>{obfuscateLicenseKey(selectedLicenseKey)}</b>
|
||||
?
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
This will remove the license key and all
|
||||
associated permissions. Any sites using this
|
||||
license key will no longer be accessible.
|
||||
</b>
|
||||
</p>
|
||||
<p>
|
||||
To confirm, please type the license key below.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonText="Confirm Delete License Key"
|
||||
onConfirm={async () => deleteLicenseKey(selectedLicenseKey)}
|
||||
string={selectedLicenseKey}
|
||||
title="Delete License Key"
|
||||
/>
|
||||
)}
|
||||
|
||||
<SettingsSectionTitle
|
||||
title="Manage License Status"
|
||||
description="View and manage license keys in the system"
|
||||
/>
|
||||
|
||||
<SettingsContainer>
|
||||
<SettingsSectionGrid cols={2}>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SSTitle>Host License</SSTitle>
|
||||
<SettingsSectionDescription>
|
||||
Manage the main license key for the host.
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
{licenseStatus?.isLicenseValid ? (
|
||||
<div className="space-y-2 text-green-500">
|
||||
<div className="text-2xl flex items-center gap-2">
|
||||
<Check />
|
||||
Licensed
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl">
|
||||
Not Licensed
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{licenseStatus?.hostId && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
Host ID
|
||||
</div>
|
||||
<CopyTextBox text={licenseStatus.hostId} />
|
||||
</div>
|
||||
)}
|
||||
{hostLicense && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
License Key
|
||||
</div>
|
||||
<CopyTextBox
|
||||
text={hostLicense}
|
||||
displayText={obfuscateLicenseKey(
|
||||
hostLicense
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={recheck}
|
||||
disabled={isRecheckingLicense}
|
||||
loading={isRecheckingLicense}
|
||||
>
|
||||
Recheck All Keys
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SSTitle>Sites Usage</SSTitle>
|
||||
<SettingsSectionDescription>
|
||||
View the number of sites using this license.
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl">
|
||||
{licenseStatus?.usedSites || 0}{" "}
|
||||
{licenseStatus?.usedSites === 1
|
||||
? "site"
|
||||
: "sites"}{" "}
|
||||
in system
|
||||
</div>
|
||||
</div>
|
||||
{licenseStatus?.maxSites && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{licenseStatus.usedSites || 0} of{" "}
|
||||
{licenseStatus.maxSites} sites used
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{Math.round(
|
||||
((licenseStatus.usedSites ||
|
||||
0) /
|
||||
licenseStatus.maxSites) *
|
||||
100
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={
|
||||
((licenseStatus.usedSites || 0) /
|
||||
licenseStatus.maxSites) *
|
||||
100
|
||||
}
|
||||
className="h-5"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SettingsSectionFooter>
|
||||
{!licenseStatus?.isHostLicensed ? (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {}}
|
||||
>
|
||||
View License Portal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPurchaseMode("license");
|
||||
setIsPurchaseModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Purchase License
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setPurchaseMode("additional-sites");
|
||||
setIsPurchaseModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Purchase Additional Sites
|
||||
</Button>
|
||||
)}
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsSectionGrid>
|
||||
<LicenseKeysDataTable
|
||||
licenseKeys={rows}
|
||||
onDelete={(key) => {
|
||||
setSelectedLicenseKey(key);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
onCreate={() => setIsCreateModalOpen(true)}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user