show features in ce

This commit is contained in:
miloschwartz
2026-02-07 17:00:44 -08:00
committed by Owen
parent ae6ed8ad97
commit fed56c1959
22 changed files with 671 additions and 707 deletions

View File

@@ -2267,6 +2267,7 @@
"actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization",
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature.",
"certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.",
"selectCertResolver": "Select Certificate Resolver",

View File

@@ -56,19 +56,29 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
}
type PostureData = {
biometricsEnabled?: boolean | null;
diskEncrypted?: boolean | null;
firewallEnabled?: boolean | null;
autoUpdatesEnabled?: boolean | null;
tpmAvailable?: boolean | null;
windowsAntivirusEnabled?: boolean | null;
macosSipEnabled?: boolean | null;
macosGatekeeperEnabled?: boolean | null;
macosFirewallStealthMode?: boolean | null;
linuxAppArmorEnabled?: boolean | null;
linuxSELinuxEnabled?: boolean | null;
biometricsEnabled?: boolean | null | "-";
diskEncrypted?: boolean | null | "-";
firewallEnabled?: boolean | null | "-";
autoUpdatesEnabled?: boolean | null | "-";
tpmAvailable?: boolean | null | "-";
windowsAntivirusEnabled?: boolean | null | "-";
macosSipEnabled?: boolean | null | "-";
macosGatekeeperEnabled?: boolean | null | "-";
macosFirewallStealthMode?: boolean | null | "-";
linuxAppArmorEnabled?: boolean | null | "-";
linuxSELinuxEnabled?: boolean | null | "-";
};
function maskPostureDataWithPlaceholder(posture: PostureData): PostureData {
const masked: PostureData = {};
for (const key of Object.keys(posture) as (keyof PostureData)[]) {
if (posture[key] !== undefined && posture[key] !== null) {
(masked as Record<keyof PostureData, "-">)[key] = "-";
}
}
return masked;
}
function getPlatformPostureData(
platform: string | null | undefined,
fingerprint: typeof currentFingerprint.$inferSelect | null
@@ -309,17 +319,19 @@ export async function getClient(
: null;
// Build posture data if available (platform-specific)
// Only return posture data if org is licensed/subscribed
let postureData: PostureData | null = null;
const isOrgLicensed = await isLicensedOrSubscribed(
client.clients.orgId
);
if (isOrgLicensed) {
postureData = getPlatformPostureData(
// Licensed: real values; not licensed: same keys but values set to "-"
const rawPosture = getPlatformPostureData(
client.currentFingerprint?.platform || null,
client.currentFingerprint
);
}
const isOrgLicensed = await isLicensedOrSubscribed(
client.clients.orgId
);
const postureData: PostureData | null = rawPosture
? isOrgLicensed
? rawPosture
: maskPostureDataWithPlaceholder(rawPosture)
: null;
const data: GetClientResponse = {
...client.clients,

View File

@@ -27,6 +27,7 @@ import {
import { Input } from "@app/components/ui/input";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -51,6 +52,7 @@ export default function Page() {
>("role");
const { isUnlocked } = useLicenseStatusContext();
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const params = useParams();
@@ -806,7 +808,7 @@ export default function Page() {
</Button>
<Button
type="submit"
disabled={createLoading}
disabled={createLoading || !isPaidUser}
loading={createLoading}
onClick={() => {
// log any issues with the form

View File

@@ -1,18 +1,8 @@
import { pullEnv } from "@app/lib/pullEnv";
import { build } from "@server/build";
import { redirect } from "next/navigation";
interface LayoutProps {
children: React.ReactNode;
params: Promise<{}>;
}
export default async function Layout(props: LayoutProps) {
const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
redirect("/");
}
return props.children;
}

View File

@@ -195,7 +195,6 @@ export default function CredentialsPage() {
</Alert>
)}
</SettingsSectionBody>
{build !== "oss" && (
<SettingsSectionFooter>
<Button
variant="outline"
@@ -217,7 +216,6 @@ export default function CredentialsPage() {
{t("remoteExitNodeRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
)}
</SettingsSection>
</SettingsContainer>

View File

@@ -61,7 +61,9 @@ export default function CredentialsPage() {
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return isEnterpriseNotLicensed || isSaasNotSubscribed;
return (
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
);
};
const handleConfirmRegenerate = async () => {
@@ -181,7 +183,6 @@ export default function CredentialsPage() {
</Alert>
)}
</SettingsSectionBody>
{build !== "oss" && (
<SettingsSectionFooter>
<Button
variant="outline"
@@ -203,7 +204,6 @@ export default function CredentialsPage() {
{t("clientRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
)}
</SettingsSection>
<OlmInstallCommands

View File

@@ -28,7 +28,15 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
import { toast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
import { useState, useEffect, useTransition } from "react";
import { Check, Ban, Shield, ShieldOff, Clock, CheckCircle2, XCircle } from "lucide-react";
import {
Check,
Ban,
Shield,
ShieldOff,
Clock,
CheckCircle2,
XCircle
} from "lucide-react";
import { useParams } from "next/navigation";
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
import { SiAndroid } from "react-icons/si";
@@ -111,13 +119,13 @@ function getPlatformFieldConfig(
osVersion: { show: true, labelKey: "iosVersion" },
kernelVersion: { show: false, labelKey: "kernelVersion" },
arch: { show: true, labelKey: "architecture" },
deviceModel: { show: true, labelKey: "deviceModel" },
deviceModel: { show: true, labelKey: "deviceModel" }
},
android: {
osVersion: { show: true, labelKey: "androidVersion" },
kernelVersion: { show: true, labelKey: "kernelVersion" },
arch: { show: true, labelKey: "architecture" },
deviceModel: { show: true, labelKey: "deviceModel" },
deviceModel: { show: true, labelKey: "deviceModel" }
},
unknown: {
osVersion: { show: true, labelKey: "osVersion" },
@@ -133,7 +141,6 @@ function getPlatformFieldConfig(
return configs[normalizedPlatform] || configs.unknown;
}
export default function GeneralPage() {
const { client, updateClient } = useClientContext();
const { isPaidUser } = usePaidStatus();
@@ -423,7 +430,8 @@ export default function GeneralPage() {
{t(
fieldConfig
.osVersion
?.labelKey || "osVersion"
?.labelKey ||
"osVersion"
)}
</InfoSectionTitle>
<InfoSectionContent>
@@ -559,8 +567,6 @@ export default function GeneralPage() {
</SettingsSection>
)}
{/* Device Security Section */}
{build !== "oss" && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
@@ -572,12 +578,14 @@ export default function GeneralPage() {
</SettingsSectionHeader>
<SettingsSectionBody>
{client.posture && Object.keys(client.posture).length > 0 ? (
<PaidFeaturesAlert />
{client.posture &&
Object.keys(client.posture).length > 0 ? (
<>
{!isPaidUser && <PaidFeaturesAlert />}
<InfoSections cols={3}>
{client.posture.biometricsEnabled !== null &&
client.posture.biometricsEnabled !== undefined && (
client.posture.biometricsEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("biometricsEnabled")}
@@ -585,7 +593,8 @@ export default function GeneralPage() {
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.biometricsEnabled
client.posture
.biometricsEnabled
)
: "-"}
</InfoSectionContent>
@@ -593,7 +602,8 @@ export default function GeneralPage() {
)}
{client.posture.diskEncrypted !== null &&
client.posture.diskEncrypted !== undefined && (
client.posture.diskEncrypted !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("diskEncrypted")}
@@ -601,7 +611,8 @@ export default function GeneralPage() {
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.diskEncrypted
client.posture
.diskEncrypted
)
: "-"}
</InfoSectionContent>
@@ -609,7 +620,8 @@ export default function GeneralPage() {
)}
{client.posture.firewallEnabled !== null &&
client.posture.firewallEnabled !== undefined && (
client.posture.firewallEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("firewallEnabled")}
@@ -617,7 +629,8 @@ export default function GeneralPage() {
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.firewallEnabled
client.posture
.firewallEnabled
)
: "-"}
</InfoSectionContent>
@@ -625,7 +638,8 @@ export default function GeneralPage() {
)}
{client.posture.autoUpdatesEnabled !== null &&
client.posture.autoUpdatesEnabled !== undefined && (
client.posture.autoUpdatesEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("autoUpdatesEnabled")}
@@ -633,7 +647,8 @@ export default function GeneralPage() {
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.autoUpdatesEnabled
client.posture
.autoUpdatesEnabled
)
: "-"}
</InfoSectionContent>
@@ -641,7 +656,8 @@ export default function GeneralPage() {
)}
{client.posture.tpmAvailable !== null &&
client.posture.tpmAvailable !== undefined && (
client.posture.tpmAvailable !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("tpmAvailable")}
@@ -649,15 +665,18 @@ export default function GeneralPage() {
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.tpmAvailable
client.posture
.tpmAvailable
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.windowsAntivirusEnabled !== null &&
client.posture.windowsAntivirusEnabled !== undefined && (
{client.posture.windowsAntivirusEnabled !==
null &&
client.posture.windowsAntivirusEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("windowsAntivirusEnabled")}
@@ -674,7 +693,8 @@ export default function GeneralPage() {
)}
{client.posture.macosSipEnabled !== null &&
client.posture.macosSipEnabled !== undefined && (
client.posture.macosSipEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("macosSipEnabled")}
@@ -682,14 +702,16 @@ export default function GeneralPage() {
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.macosSipEnabled
client.posture
.macosSipEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosGatekeeperEnabled !== null &&
{client.posture.macosGatekeeperEnabled !==
null &&
client.posture.macosGatekeeperEnabled !==
undefined && (
<InfoSection>
@@ -707,7 +729,8 @@ export default function GeneralPage() {
</InfoSection>
)}
{client.posture.macosFirewallStealthMode !== null &&
{client.posture.macosFirewallStealthMode !==
null &&
client.posture.macosFirewallStealthMode !==
undefined && (
<InfoSection>
@@ -769,7 +792,6 @@ export default function GeneralPage() {
)}
</SettingsSectionBody>
</SettingsSection>
)}
</SettingsContainer>
);
}

View File

@@ -20,11 +20,6 @@ export interface AuthPageProps {
export default async function AuthPage(props: AuthPageProps) {
const orgId = (await props.params).orgId;
// custom auth branding is only available in enterprise and saas
if (build === "oss") {
redirect(`/${orgId}/settings/general/`);
}
let subscriptionStatus: GetOrgTierResponse | null = null;
try {
const subRes = await getCachedSubscription(orgId);

View File

@@ -55,14 +55,12 @@ export default async function GeneralSettingsPage({
{
title: t("security"),
href: `/{orgId}/settings/general/security`
}
];
if (build !== "oss") {
navItems.push({
},
{
title: t("authPage"),
href: `/{orgId}/settings/general/auth-page`
});
}
];
return (
<>

View File

@@ -3,12 +3,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { Button } from "@app/components/ui/button";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast";
import {
useState,
useRef,
useActionState,
type ComponentRef
} from "react";
import { useState, useRef, useActionState, type ComponentRef } from "react";
import {
Form,
FormControl,
@@ -110,7 +105,7 @@ export default function SecurityPage() {
return (
<SettingsContainer>
<LogRetentionSectionForm org={org.org} />
{build !== "oss" && <SecuritySettingsSectionForm org={org.org} />}
<SecuritySettingsSectionForm org={org.org} />
</SettingsContainer>
);
}
@@ -243,8 +238,6 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
)}
/>
{build !== "oss" && (
<>
<PaidFeaturesAlert />
<FormField
@@ -256,19 +249,13 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
return (
<FormItem>
<FormLabel>
{t(
"logRetentionAccessLabel"
)}
{t("logRetentionAccessLabel")}
</FormLabel>
<FormControl>
<Select
value={field.value.toString()}
onValueChange={(
value
) => {
if (
!isDisabled
) {
onValueChange={(value) => {
if (!isDisabled) {
field.onChange(
parseInt(
value,
@@ -277,9 +264,7 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
);
}
}}
disabled={
isDisabled
}
disabled={isDisabled}
>
<SelectTrigger>
<SelectValue
@@ -290,9 +275,7 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
</SelectTrigger>
<SelectContent>
{LOG_RETENTION_OPTIONS.map(
(
option
) => (
(option) => (
<SelectItem
key={
option.value
@@ -322,19 +305,13 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
return (
<FormItem>
<FormLabel>
{t(
"logRetentionActionLabel"
)}
{t("logRetentionActionLabel")}
</FormLabel>
<FormControl>
<Select
value={field.value.toString()}
onValueChange={(
value
) => {
if (
!isDisabled
) {
onValueChange={(value) => {
if (!isDisabled) {
field.onChange(
parseInt(
value,
@@ -343,9 +320,7 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
);
}
}}
disabled={
isDisabled
}
disabled={isDisabled}
>
<SelectTrigger>
<SelectValue
@@ -356,9 +331,7 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
</SelectTrigger>
<SelectContent>
{LOG_RETENTION_OPTIONS.map(
(
option
) => (
(option) => (
<SelectItem
key={
option.value
@@ -379,8 +352,6 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
);
}}
/>
</>
)}
</form>
</Form>
</SettingsSectionForm>
@@ -740,7 +711,7 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
type="submit"
form="security-settings-section-form"
loading={loadingSave}
disabled={loadingSave}
disabled={loadingSave || !isPaidUser}
>
{t("saveSettings")}
</Button>

View File

@@ -20,6 +20,7 @@ import { Alert, AlertDescription } from "@app/components/ui/alert";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
import axios from "axios";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
export default function GeneralPage() {
const router = useRouter();
@@ -209,7 +210,8 @@ export default function GeneralPage() {
console.log("Date range changed:", { startDate, endDate, page, size });
if (
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked())
(build == "enterprise" && !isUnlocked()) ||
build === "oss"
) {
console.log(
"Access denied: subscription inactive or license locked"
@@ -611,21 +613,7 @@ export default function GeneralPage() {
description={t("accessLogsDescription")}
/>
{build == "saas" && !subscription?.subscribed ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("subscriptionRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
{build == "enterprise" && !isUnlocked() ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("licenseRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
<PaidFeaturesAlert />
<LogDataTable
columns={columns}
@@ -656,7 +644,8 @@ export default function GeneralPage() {
renderExpandedRow={renderExpandedRow}
disabled={
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked())
(build == "enterprise" && !isUnlocked()) ||
build === "oss"
}
/>
</>

View File

@@ -2,6 +2,7 @@
import { ColumnFilter } from "@app/components/ColumnFilter";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -92,6 +93,9 @@ export default function GeneralPage() {
// Trigger search with default values on component mount
useEffect(() => {
if (build === "oss") {
return;
}
const defaultRange = getDefaultDateRange();
queryDateTime(
defaultRange.startDate,
@@ -461,21 +465,7 @@ export default function GeneralPage() {
description={t("actionLogsDescription")}
/>
{build == "saas" && !subscription?.subscribed ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("subscriptionRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
{build == "enterprise" && !isUnlocked() ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("licenseRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
<PaidFeaturesAlert />
<LogDataTable
columns={columns}
@@ -508,7 +498,8 @@ export default function GeneralPage() {
renderExpandedRow={renderExpandedRow}
disabled={
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked())
(build == "enterprise" && !isUnlocked()) ||
build === "oss"
}
/>
</>

View File

@@ -16,6 +16,7 @@ import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState, useTransition } from "react";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { build } from "@server/build";
export default function GeneralPage() {
const router = useRouter();
@@ -110,6 +111,9 @@ export default function GeneralPage() {
// Trigger search with default values on component mount
useEffect(() => {
if (build === "oss") {
return;
}
const defaultRange = getDefaultDateRange();
queryDateTime(
defaultRange.startDate,

View File

@@ -63,6 +63,7 @@ import {
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { GetResourceResponse } from "@server/routers/resource/getResource";
import type { ResourceContextType } from "@app/contexts/resourceContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
type MaintenanceSectionFormProps = {
resource: GetResourceResponse;
@@ -78,6 +79,7 @@ function MaintenanceSectionForm({
const api = createApiClient({ env });
const { isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext();
const { isPaidUser } = usePaidStatus();
const MaintenanceFormSchema = z.object({
maintenanceModeEnabled: z.boolean().optional(),
@@ -161,7 +163,7 @@ function MaintenanceSectionForm({
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return isEnterpriseNotLicensed || isSaasNotSubscribed;
return isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss";
};
if (!resource.http) {
@@ -413,7 +415,7 @@ function MaintenanceSectionForm({
<Button
type="submit"
loading={maintenanceSaveLoading}
disabled={maintenanceSaveLoading}
disabled={maintenanceSaveLoading || !isPaidUser }
form="maintenance-settings-form"
>
{t("saveSettings")}
@@ -739,12 +741,10 @@ export default function GeneralForm() {
</SettingsSectionFooter>
</SettingsSection>
{build !== "oss" && (
<MaintenanceSectionForm
resource={resource}
updateResource={updateResource}
/>
)}
</SettingsContainer>
<Credenza

View File

@@ -72,7 +72,9 @@ export default function CredentialsPage() {
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return isEnterpriseNotLicensed || isSaasNotSubscribed;
return (
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
);
};
// Fetch site defaults for wireguard sites to show in obfuscated config
@@ -269,7 +271,6 @@ export default function CredentialsPage() {
</Alert>
)}
</SettingsSectionBody>
{build !== "oss" && (
<SettingsSectionFooter>
<Button
variant="outline"
@@ -291,7 +292,6 @@ export default function CredentialsPage() {
{t("siteRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
)}
</SettingsSection>
<NewtSiteInstallCommands
@@ -383,7 +383,6 @@ export default function CredentialsPage() {
</>
)}
</SettingsSectionBody>
{build === "enterprise" && (
<SettingsSectionFooter>
<Button
onClick={() => setModalOpen(true)}
@@ -392,7 +391,6 @@ export default function CredentialsPage() {
{t("siteRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
)}
</SettingsSection>
)}
</SettingsContainer>

View File

@@ -121,24 +121,16 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
href: "/{orgId}/settings/access/roles",
icon: <Users className="size-4 flex-none" />
},
...(build === "saas" || env?.flags.useOrgOnlyIdp
? [
{
title: "sidebarIdentityProviders",
href: "/{orgId}/settings/idp",
icon: <Fingerprint className="size-4 flex-none" />
}
]
: []),
...(build !== "oss"
? [
},
{
title: "sidebarApprovals",
href: "/{orgId}/settings/access/approvals",
icon: <UserCog className="size-4 flex-none" />
}
]
: []),
},
{
title: "sidebarShareableLinks",
href: "/{orgId}/settings/share-links",
@@ -155,8 +147,6 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
href: "/{orgId}/settings/logs/request",
icon: <SquareMousePointer className="size-4 flex-none" />
},
...(build != "oss"
? [
{
title: "sidebarLogsAccess",
href: "/{orgId}/settings/logs/access",
@@ -167,8 +157,6 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
href: "/{orgId}/settings/logs/action",
icon: <Logs className="size-4 flex-none" />
}
]
: [])
];
const analytics = {

View File

@@ -30,6 +30,7 @@ import {
import { Separator } from "./ui/separator";
import { InfoPopup } from "./ui/info-popup";
import { ApprovalsEmptyState } from "./ApprovalsEmptyState";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
export type ApprovalFeedProps = {
orgId: string;
@@ -50,9 +51,12 @@ export function ApprovalFeed({
Object.fromEntries(searchParams.entries())
);
const { data, isFetching, refetch } = useQuery(
approvalQueries.listApprovals(orgId, filters)
);
const { isPaidUser } = usePaidStatus();
const { data, isFetching, refetch } = useQuery({
...approvalQueries.listApprovals(orgId, filters),
enabled: isPaidUser
});
const approvals = data?.approvals ?? [];
@@ -229,7 +233,10 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
{t("deviceInformation")}
</div>
<div className="text-muted-foreground whitespace-pre-line">
{formatFingerprintInfo(approval.fingerprint, t)}
{formatFingerprintInfo(
approval.fingerprint,
t
)}
</div>
</div>
</InfoPopup>

View File

@@ -160,8 +160,7 @@ export default function CreateRoleForm({
</FormItem>
)}
/>
{build !== "oss" && (
<div>
<PaidFeaturesAlert />
<FormField
@@ -172,9 +171,7 @@ export default function CreateRoleForm({
<FormControl>
<CheckboxWithLabel
{...field}
disabled={
!isPaidUser
}
disabled={!isPaidUser}
value="on"
checked={form.watch(
"requireDeviceApproval"
@@ -208,8 +205,6 @@ export default function CreateRoleForm({
</FormItem>
)}
/>
</div>
)}
</form>
</Form>
</CredenzaBody>

View File

@@ -168,8 +168,6 @@ export default function EditRoleForm({
</FormItem>
)}
/>
{build !== "oss" && (
<div>
<PaidFeaturesAlert />
<FormField
@@ -180,9 +178,7 @@ export default function EditRoleForm({
<FormControl>
<CheckboxWithLabel
{...field}
disabled={
!isPaidUser
}
disabled={!isPaidUser}
value="on"
checked={form.watch(
"requireDeviceApproval"
@@ -216,8 +212,6 @@ export default function EditRoleForm({
</FormItem>
)}
/>
</div>
)}
</form>
</Form>
</CredenzaBody>

View File

@@ -1,8 +1,16 @@
"use client";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { Card, CardContent } from "@app/components/ui/card";
import { build } from "@server/build";
import { useTranslations } from "next-intl";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { ExternalLink, KeyRound, Sparkles } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
const bannerClassName =
"mb-6 border-primary/30 bg-linear-to-br from-primary/10 via-background to-background overflow-hidden";
const bannerContentClassName = "py-3 px-4";
const bannerRowClassName =
"flex items-center gap-2.5 text-sm text-muted-foreground";
export function PaidFeaturesAlert() {
const t = useTranslations();
@@ -10,19 +18,50 @@ export function PaidFeaturesAlert() {
return (
<>
{build === "saas" && !hasSaasSubscription ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("subscriptionRequiredToUse")}
</AlertDescription>
</Alert>
<Card className={bannerClassName}>
<CardContent className={bannerContentClassName}>
<div className={bannerRowClassName}>
<KeyRound className="size-4 shrink-0 text-primary" />
<span>{t("subscriptionRequiredToUse")}</span>
</div>
</CardContent>
</Card>
) : null}
{build === "enterprise" && !hasEnterpriseLicense ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("licenseRequiredToUse")}
</AlertDescription>
</Alert>
<Card className={bannerClassName}>
<CardContent className={bannerContentClassName}>
<div className={bannerRowClassName}>
<KeyRound className="size-4 shrink-0 text-primary" />
<span>{t("licenseRequiredToUse")}</span>
</div>
</CardContent>
</Card>
) : null}
{build === "oss" && !hasEnterpriseLicense ? (
<Card className={bannerClassName}>
<CardContent className={bannerContentClassName}>
<div className={bannerRowClassName}>
<KeyRound className="size-4 shrink-0 text-primary" />
<span>
{t.rich("ossEnterpriseEditionRequired", {
enterpriseEditionLink: (chunks) => (
<Link
href="https://docs.pangolin.net/self-host/enterprise-edition"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-foreground underline"
>
{chunks}
<ExternalLink className="size-3.5 shrink-0" />
</Link>
)
})}
</span>
</div>
</CardContent>
</Card>
) : null}
</>
);

View File

@@ -548,7 +548,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{clientRow.approvalState === "pending" && build !== "oss" && (
{clientRow.approvalState === "pending" && (
<>
<DropdownMenuItem
onClick={() => approveDevice(clientRow)}
@@ -652,17 +652,10 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
}
];
if (build === "oss") {
return allOptions.filter((option) => option.value !== "pending" && option.value !== "denied");
}
return allOptions;
}, [t]);
const statusFilterDefaultValues = useMemo(() => {
if (build === "oss") {
return ["active"];
}
return ["active", "pending"];
}, []);

View File

@@ -2,29 +2,6 @@ import { NextRequest, NextResponse } from "next/server";
import { build } from "@server/build";
export function middleware(request: NextRequest) {
// If build is OSS, block access to private routes
if (build === "oss") {
const pathname = request.nextUrl.pathname;
// Define private route patterns that should be blocked in OSS build
const privateRoutes = [
"/settings/billing",
"/settings/remote-exit-nodes",
"/settings/idp",
"/auth/org"
];
// Check if current path matches any private route pattern
const isPrivateRoute = privateRoutes.some((route) =>
pathname.includes(route)
);
if (isPrivateRoute) {
// Return 404 to make it seem like the route doesn't exist
return new NextResponse(null, { status: 404 });
}
}
return NextResponse.next();
}