add feature parity

This commit is contained in:
miloschwartz
2025-05-13 11:09:38 -04:00
parent a512148348
commit 5b0200154a
92 changed files with 353 additions and 759 deletions

View File

@@ -42,7 +42,7 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
const formSchema = z.object({
email: z.string().email({ message: "Please enter a valid email" }),
username: z.string(),
roleId: z.string().min(1, { message: "Please select a role" })
});
@@ -59,7 +59,7 @@ export default function AccessControlsPage() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: user.email!,
username: user.username!,
roleId: user.roleId?.toString()
}
});

View File

@@ -1,8 +1,3 @@
// 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 { DataTable } from "@app/components/ui/data-table";

View File

@@ -1,8 +1,3 @@
// 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";

View File

@@ -1,8 +1,3 @@
// 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 { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";

View File

@@ -1,8 +1,3 @@
// 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 { redirect } from "next/navigation";
export default async function ApiKeysPage(props: {

View File

@@ -1,8 +1,3 @@
// 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 PermissionsSelectBox from "@app/components/PermissionsSelectBox";

View File

@@ -1,8 +1,3 @@
// 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 {

View File

@@ -1,8 +1,3 @@
// 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 { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { AxiosResponse } from "axios";

View File

@@ -1,8 +1,6 @@
"use client";
import {
ColumnDef,
} from "@tanstack/react-table";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
interface DataTableProps<TData, TValue> {
@@ -25,6 +23,10 @@ export function ResourcesDataTable<TData, TValue>({
searchColumn="name"
onAdd={createResource}
addButtonText="Add Resource"
defaultSort={{
id: "name",
desc: false
}}
/>
);
}

View File

@@ -1,8 +1,6 @@
"use client";
import {
ColumnDef,
} from "@tanstack/react-table";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@app/components/ui/data-table";
interface DataTableProps<TData, TValue> {
@@ -25,6 +23,10 @@ export function SitesDataTable<TData, TValue>({
searchColumn="name"
onAdd={createSite}
addButtonText="Add Site"
defaultSort={{
id: "name",
desc: false
}}
/>
);
}

View File

@@ -1,8 +1,3 @@
// 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 {

View File

@@ -1,8 +1,3 @@
// 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";

View File

@@ -1,8 +1,3 @@
// 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 { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";

View File

@@ -1,8 +1,3 @@
// 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 { redirect } from "next/navigation";
export default async function ApiKeysPage(props: {

View File

@@ -1,8 +1,3 @@
// 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 PermissionsSelectBox from "@app/components/PermissionsSelectBox";

View File

@@ -1,8 +1,3 @@
// 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 {

View File

@@ -1,8 +1,3 @@
// 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 { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { AxiosResponse } from "axios";

View File

@@ -232,7 +232,6 @@ export default function GeneralPage() {
defaultChecked={form.getValues(
"autoProvision"
)}
disabled={!isUnlocked()}
onCheckedChange={(checked) => {
form.setValue(
"autoProvision",
@@ -240,14 +239,6 @@ export default function GeneralPage() {
);
}}
/>
{!isUnlocked() && (
<Badge
variant="outlinePrimary"
className="ml-2"
>
Professional
</Badge>
)}
</div>
<span className="text-sm text-muted-foreground">
When enabled, users will be

View File

@@ -43,8 +43,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
},
{
title: "Organization Policies",
href: `/admin/idp/${params.idpId}/policies`,
showProfessional: true
href: `/admin/idp/${params.idpId}/policies`
}
];

View File

@@ -1,8 +1,3 @@
// 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";

View File

@@ -1,8 +1,3 @@
// 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";

View File

@@ -1,8 +1,3 @@
// 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 { useEffect, useState } from "react";
@@ -377,9 +372,7 @@ export default function PoliciesPage() {
<Input {...field} />
</FormControl>
<FormDescription>
JMESPath to extract role
information from the ID
token. The result of this
The result of this
expression must return the
role name as defined in the
organization as a string.
@@ -401,13 +394,10 @@ export default function PoliciesPage() {
<Input {...field} />
</FormControl>
<FormDescription>
JMESPath to extract
organization information
from the ID token. This
expression must return thr
org ID or true for the user
to be allowed to access the
organization.
This expression must return
thr org ID or true for the
user to be allowed to access
the organization.
</FormDescription>
<FormMessage />
</FormItem>
@@ -578,8 +568,6 @@ export default function PoliciesPage() {
<Input {...field} />
</FormControl>
<FormDescription>
JMESPath to extract role
information from the ID token.
The result of this expression
must return the role name as
defined in the organization as a
@@ -603,8 +591,6 @@ export default function PoliciesPage() {
<Input {...field} />
</FormControl>
<FormDescription>
JMESPath to extract organization
information from the ID token.
This expression must return the
org ID or true for the user to
be allowed to access the

View File

@@ -192,7 +192,6 @@ export default function Page() {
defaultChecked={form.getValues(
"autoProvision"
)}
disabled={!isUnlocked()}
onCheckedChange={(checked) => {
form.setValue(
"autoProvision",
@@ -200,14 +199,6 @@ export default function Page() {
);
}}
/>
{!isUnlocked() && (
<Badge
variant="outlinePrimary"
className="ml-2"
>
Professional
</Badge>
)}
</div>
<span className="text-sm text-muted-foreground">
When enabled, users will be
@@ -421,7 +412,7 @@ export default function Page() {
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the user
The path to the user
identifier in the ID
token
</FormDescription>
@@ -442,7 +433,7 @@ export default function Page() {
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
The path to the
user's email in the ID
token
</FormDescription>
@@ -463,7 +454,7 @@ export default function Page() {
<Input {...field} />
</FormControl>
<FormDescription>
The JMESPath to the
The path to the
user's name in the ID
token
</FormDescription>

View File

@@ -1,8 +1,3 @@
// 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";

View File

@@ -1,8 +1,3 @@
// 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";
@@ -107,35 +102,6 @@ export function SitePriceCalculator({
</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>
<p className="text-muted-foreground text-sm mt-2 text-center">
For the most up-to-date pricing and discounts,
please visit the{" "}
@@ -157,7 +123,7 @@ export function SitePriceCalculator({
<Button variant="outline">Cancel</Button>
</CredenzaClose>
<Button onClick={continueToPayment}>
Continue to Payment
See Purchase Portal
</Button>
</CredenzaFooter>
</CredenzaContent>

View File

@@ -1,8 +1,3 @@
// 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";
@@ -49,7 +44,7 @@ import {
} 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 { Check, Heart, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
import CopyTextBox from "@app/components/CopyTextBox";
import { Progress } from "@app/components/ui/progress";
import { MinusCircle, PlusCircle } from "lucide-react";
@@ -57,6 +52,8 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { SitePriceCalculator } from "./components/SitePriceCalculator";
import Link from "next/link";
import { Checkbox } from "@app/components/ui/checkbox";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
const formSchema = z.object({
licenseKey: z
@@ -95,6 +92,7 @@ export default function LicensePage() {
const [isActivatingLicense, setIsActivatingLicense] = useState(false);
const [isDeletingLicense, setIsDeletingLicense] = useState(false);
const [isRecheckingLicense, setIsRecheckingLicense] = useState(false);
const { supporterStatus } = useSupporterStatusContext();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -370,6 +368,18 @@ export default function LicensePage() {
description="View and manage license keys in the system"
/>
<Alert variant="neutral" className="mb-6">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
About Licensing
</AlertTitle>
<AlertDescription>
This is for business and enterprise users who are using
Pangolin in a commercial environment. If you are using
Pangolin for personal use, you can ignore this section.
</AlertDescription>
</Alert>
<SettingsContainer>
<SettingsSectionGrid cols={2}>
<SettingsSection>
@@ -387,18 +397,25 @@ export default function LicensePage() {
<Check />
{licenseStatus?.tier ===
"PROFESSIONAL"
? "Professional License"
? "Commercial License"
: licenseStatus?.tier ===
"ENTERPRISE"
? "Enterprise License"
? "Commercial License"
: "Licensed"}
</div>
</div>
) : (
<div className="space-y-2">
<div className="text-2xl">
Not Licensed
</div>
{supporterStatus?.visible ? (
<div className="text-2xl">
Community Edition
</div>
) : (
<div className="text-2xl flex items-center gap-2 text-pink-500">
<Heart />
Community Edition
</div>
)}
</div>
)}
</div>

View File

@@ -1,6 +1,11 @@
import ProfileIcon from "@app/components/ProfileIcon";
import { Separator } from "@app/components/ui/separator";
import { priv } from "@app/lib/api";
import { verifySession } from "@app/lib/auth/verifySession";
import UserProvider from "@app/providers/UserProvider";
import { GetLicenseStatusResponse } from "@server/routers/license";
import { AxiosResponse } from "axios";
import { ExternalLink } from "lucide-react";
import { Metadata } from "next";
import { cache } from "react";
@@ -17,6 +22,14 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
const getUser = cache(verifySession);
const user = await getUser();
const licenseStatusRes = await cache(
async () =>
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
"/license/status"
)
)();
const licenseStatus = licenseStatusRes.data.data;
return (
<div className="h-full flex flex-col">
{user && (
@@ -28,10 +41,49 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
)}
<div className="flex-1 flex items-center justify-center">
<div className="w-full max-w-md p-3">
{children}
</div>
<div className="w-full max-w-md p-3">{children}</div>
</div>
{!(
licenseStatus.isHostLicensed && licenseStatus.isLicenseValid
) && (
<footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600">
<div className="flex items-center space-x-2 whitespace-nowrap">
<span>Pangolin</span>
</div>
<Separator orientation="vertical" />
<a
href="https://fossorial.io/"
target="_blank"
rel="noopener noreferrer"
aria-label="Built by Fossorial"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>Fossorial</span>
<ExternalLink className="w-3 h-3" />
</a>
<Separator orientation="vertical" />
<a
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="flex items-center space-x-2 whitespace-nowrap"
>
<span>Community Edition</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-3 h-3"
>
<path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.385-1.333-1.755-1.333-1.755-1.09-.744.082-.73.082-.73 1.205.085 1.84 1.24 1.84 1.24 1.07 1.835 2.807 1.305 3.492.997.107-.775.42-1.305.763-1.605-2.665-.305-5.467-1.335-5.467-5.93 0-1.31.468-2.382 1.236-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23a11.52 11.52 0 013.006-.403c1.02.005 2.045.137 3.006.403 2.29-1.552 3.295-1.23 3.295-1.23.654 1.653.242 2.873.12 3.176.77.838 1.235 1.91 1.235 3.22 0 4.605-2.805 5.623-5.475 5.92.43.37.814 1.1.814 2.22v3.293c0 .32.217.693.825.576C20.565 21.795 24 17.298 24 12 24 5.373 18.627 0 12 0z" />
</svg>
</a>
</div>
</footer>
)}
</div>
);
}

View File

@@ -1,8 +1,3 @@
// 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 { Button } from "@app/components/ui/button";

View File

@@ -12,6 +12,7 @@ import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey";
import LicenseStatusProvider from "@app/providers/LicenseStatusProvider";
import { GetLicenseStatusResponse } from "@server/routers/license";
import LicenseViolation from "./components/LicenseViolation";
import { cache } from "react";
export const metadata: Metadata = {
title: `Dashboard - Pangolin`,
@@ -40,10 +41,12 @@ export default async function RootLayout({
supporterData.visible = res.data.data.visible;
supporterData.tier = res.data.data.tier;
const licenseStatusRes =
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
"/license/status"
);
const licenseStatusRes = await cache(
async () =>
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
"/license/status"
)
)();
const licenseStatus = licenseStatusRes.data.data;
return (

View File

@@ -68,8 +68,7 @@ export const orgNavItems: SidebarNavItem[] = [
{
title: "API Keys",
href: "/{orgId}/settings/api-keys",
icon: <KeyRound className="h-4 w-4" />,
showProfessional: true
icon: <KeyRound className="h-4 w-4" />
},
{
title: "Settings",
@@ -87,8 +86,7 @@ export const adminNavItems: SidebarNavItem[] = [
{
title: "API Keys",
href: "/admin/api-keys",
icon: <KeyRound className="h-4 w-4" />,
showProfessional: true
icon: <KeyRound className="h-4 w-4" />
},
{
title: "Identity Providers",

View File

@@ -0,0 +1,3 @@
"use client";
export function AuthFooter() {}

View File

@@ -22,6 +22,7 @@ import { Breadcrumbs } from "@app/components/Breadcrumbs";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
interface LayoutProps {
children: React.ReactNode;
@@ -58,6 +59,7 @@ export function Layout({
const pathname = usePathname();
const isAdminPage = pathname?.startsWith("/admin");
const { user } = useUserContext();
const { isUnlocked } = useLicenseStatusContext();
return (
<div className="flex flex-col h-screen overflow-hidden">
@@ -207,7 +209,9 @@ export function Layout({
rel="noopener noreferrer"
className="flex items-center justify-center gap-1"
>
Open Source
{!isUnlocked()
? "Community Edition"
: "Commercial Edition"}
<ExternalLink size={12} />
</Link>
</div>

View File

@@ -1,8 +1,3 @@
// 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 { CheckboxWithLabel } from "@app/components/ui/checkbox";
@@ -26,7 +21,6 @@ function getActionsCategories(root: boolean) {
"Update Organization": "updateOrg",
"Get Organization User": "getOrgUser",
"List Organization Domains": "listOrgDomains",
"Check Org ID": "checkOrgId",
},
Site: {

View File

@@ -1,8 +1,3 @@
// 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 { cn } from "@app/lib/cn";

View File

@@ -193,7 +193,7 @@ export default function SupporterStatus() {
contribution allows us to commit more time to
maintain and add new features to the application for
everyone. We will never use this to paywall
features. This is separate from the Professional
features. This is separate from any Commercial
Edition.
</p>

View File

@@ -31,7 +31,7 @@ import {
CardTitle
} from "@app/components/ui/card";
interface DataTableProps<TData, TValue> {
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
title?: string;
@@ -39,7 +39,11 @@ interface DataTableProps<TData, TValue> {
onAdd?: () => void;
searchPlaceholder?: string;
searchColumn?: string;
}
defaultSort?: {
id: string;
desc: boolean;
};
};
export function DataTable<TData, TValue>({
columns,
@@ -48,9 +52,12 @@ export function DataTable<TData, TValue>({
addButtonText,
onAdd,
searchPlaceholder = "Search...",
searchColumn = "name"
searchColumn = "name",
defaultSort
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [sorting, setSorting] = useState<SortingState>(
defaultSort ? [defaultSort] : []
);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState<any>([]);

View File

@@ -1,8 +1,3 @@
// 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 { GetApiKeyResponse } from "@server/routers/apiKeys";
import { createContext } from "react";

View File

@@ -1,8 +1,3 @@
// 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 { LicenseStatus } from "@server/license/license";
import { createContext } from "react";

View File

@@ -1,8 +1,3 @@
// 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 ApiKeyContext from "@app/contexts/apiKeyContext";
import { useContext } from "react";

View File

@@ -1,8 +1,3 @@
// 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 LicenseStatusContext from "@app/contexts/licenseStatusContext";
import { useContext } from "react";

View File

@@ -1,8 +1,3 @@
// 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 ApiKeyContext from "@app/contexts/apiKeyContext";

View File

@@ -1,8 +1,3 @@
// 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 LicenseStatusContext from "@app/contexts/licenseStatusContext";