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", "actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization", "accessLogsDescription": "View access auth requests for resources in this organization",
"licenseRequiredToUse": "An Enterprise license is required to use this feature.", "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", "certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.", "certResolverDescription": "Select the certificate resolver to use for this resource.",
"selectCertResolver": "Select Certificate Resolver", "selectCertResolver": "Select Certificate Resolver",

View File

@@ -56,19 +56,29 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
} }
type PostureData = { type PostureData = {
biometricsEnabled?: boolean | null; biometricsEnabled?: boolean | null | "-";
diskEncrypted?: boolean | null; diskEncrypted?: boolean | null | "-";
firewallEnabled?: boolean | null; firewallEnabled?: boolean | null | "-";
autoUpdatesEnabled?: boolean | null; autoUpdatesEnabled?: boolean | null | "-";
tpmAvailable?: boolean | null; tpmAvailable?: boolean | null | "-";
windowsAntivirusEnabled?: boolean | null; windowsAntivirusEnabled?: boolean | null | "-";
macosSipEnabled?: boolean | null; macosSipEnabled?: boolean | null | "-";
macosGatekeeperEnabled?: boolean | null; macosGatekeeperEnabled?: boolean | null | "-";
macosFirewallStealthMode?: boolean | null; macosFirewallStealthMode?: boolean | null | "-";
linuxAppArmorEnabled?: boolean | null; linuxAppArmorEnabled?: boolean | null | "-";
linuxSELinuxEnabled?: 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( function getPlatformPostureData(
platform: string | null | undefined, platform: string | null | undefined,
fingerprint: typeof currentFingerprint.$inferSelect | null fingerprint: typeof currentFingerprint.$inferSelect | null
@@ -294,32 +304,34 @@ export async function getClient(
// Build fingerprint data if available // Build fingerprint data if available
const fingerprintData = client.currentFingerprint const fingerprintData = client.currentFingerprint
? { ? {
username: client.currentFingerprint.username || null, username: client.currentFingerprint.username || null,
hostname: client.currentFingerprint.hostname || null, hostname: client.currentFingerprint.hostname || null,
platform: client.currentFingerprint.platform || null, platform: client.currentFingerprint.platform || null,
osVersion: client.currentFingerprint.osVersion || null, osVersion: client.currentFingerprint.osVersion || null,
kernelVersion: kernelVersion:
client.currentFingerprint.kernelVersion || null, client.currentFingerprint.kernelVersion || null,
arch: client.currentFingerprint.arch || null, arch: client.currentFingerprint.arch || null,
deviceModel: client.currentFingerprint.deviceModel || null, deviceModel: client.currentFingerprint.deviceModel || null,
serialNumber: client.currentFingerprint.serialNumber || null, serialNumber: client.currentFingerprint.serialNumber || null,
firstSeen: client.currentFingerprint.firstSeen || null, firstSeen: client.currentFingerprint.firstSeen || null,
lastSeen: client.currentFingerprint.lastSeen || null lastSeen: client.currentFingerprint.lastSeen || null
} }
: null; : null;
// Build posture data if available (platform-specific) // Build posture data if available (platform-specific)
// Only return posture data if org is licensed/subscribed // Licensed: real values; not licensed: same keys but values set to "-"
let postureData: PostureData | null = null; const rawPosture = getPlatformPostureData(
client.currentFingerprint?.platform || null,
client.currentFingerprint
);
const isOrgLicensed = await isLicensedOrSubscribed( const isOrgLicensed = await isLicensedOrSubscribed(
client.clients.orgId client.clients.orgId
); );
if (isOrgLicensed) { const postureData: PostureData | null = rawPosture
postureData = getPlatformPostureData( ? isOrgLicensed
client.currentFingerprint?.platform || null, ? rawPosture
client.currentFingerprint : maskPostureDataWithPlaceholder(rawPosture)
); : null;
}
const data: GetClientResponse = { const data: GetClientResponse = {
...client.clients, ...client.clients,

View File

@@ -27,6 +27,7 @@ import {
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -51,6 +52,7 @@ export default function Page() {
>("role"); >("role");
const { isUnlocked } = useLicenseStatusContext(); const { isUnlocked } = useLicenseStatusContext();
const t = useTranslations(); const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const params = useParams(); const params = useParams();
@@ -806,7 +808,7 @@ export default function Page() {
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={createLoading} disabled={createLoading || !isPaidUser}
loading={createLoading} loading={createLoading}
onClick={() => { onClick={() => {
// log any issues with the form // 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 { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
params: Promise<{}>; params: Promise<{}>;
} }
export default async function Layout(props: LayoutProps) { export default async function Layout(props: LayoutProps) {
const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
redirect("/");
}
return props.children; return props.children;
} }

View File

@@ -195,29 +195,27 @@ export default function CredentialsPage() {
</Alert> </Alert>
)} )}
</SettingsSectionBody> </SettingsSectionBody>
{build !== "oss" && ( <SettingsSectionFooter>
<SettingsSectionFooter> <Button
<Button variant="outline"
variant="outline" onClick={() => {
onClick={() => { setShouldDisconnect(false);
setShouldDisconnect(false); setModalOpen(true);
setModalOpen(true); }}
}} disabled={isSecurityFeatureDisabled()}
disabled={isSecurityFeatureDisabled()} >
> {t("regenerateCredentialsButton")}
{t("regenerateCredentialsButton")} </Button>
</Button> <Button
<Button onClick={() => {
onClick={() => { setShouldDisconnect(true);
setShouldDisconnect(true); setModalOpen(true);
setModalOpen(true); }}
}} disabled={isSecurityFeatureDisabled()}
disabled={isSecurityFeatureDisabled()} >
> {t("remoteExitNodeRegenerateAndDisconnect")}
{t("remoteExitNodeRegenerateAndDisconnect")} </Button>
</Button> </SettingsSectionFooter>
</SettingsSectionFooter>
)}
</SettingsSection> </SettingsSection>
</SettingsContainer> </SettingsContainer>

View File

@@ -61,7 +61,9 @@ export default function CredentialsPage() {
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed = const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed(); build === "saas" && !subscription?.isSubscribed();
return isEnterpriseNotLicensed || isSaasNotSubscribed; return (
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
);
}; };
const handleConfirmRegenerate = async () => { const handleConfirmRegenerate = async () => {
@@ -181,29 +183,27 @@ export default function CredentialsPage() {
</Alert> </Alert>
)} )}
</SettingsSectionBody> </SettingsSectionBody>
{build !== "oss" && ( <SettingsSectionFooter>
<SettingsSectionFooter> <Button
<Button variant="outline"
variant="outline" onClick={() => {
onClick={() => { setShouldDisconnect(false);
setShouldDisconnect(false); setModalOpen(true);
setModalOpen(true); }}
}} disabled={isSecurityFeatureDisabled()}
disabled={isSecurityFeatureDisabled()} >
> {t("regenerateCredentialsButton")}
{t("regenerateCredentialsButton")} </Button>
</Button> <Button
<Button onClick={() => {
onClick={() => { setShouldDisconnect(true);
setShouldDisconnect(true); setModalOpen(true);
setModalOpen(true); }}
}} disabled={isSecurityFeatureDisabled()}
disabled={isSecurityFeatureDisabled()} >
> {t("clientRegenerateAndDisconnect")}
{t("clientRegenerateAndDisconnect")} </Button>
</Button> </SettingsSectionFooter>
</SettingsSectionFooter>
)}
</SettingsSection> </SettingsSection>
<OlmInstallCommands <OlmInstallCommands

View File

@@ -28,7 +28,15 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState, useEffect, useTransition } from "react"; 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 { useParams } from "next/navigation";
import { FaApple, FaWindows, FaLinux } from "react-icons/fa"; import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
import { SiAndroid } from "react-icons/si"; import { SiAndroid } from "react-icons/si";
@@ -111,13 +119,13 @@ function getPlatformFieldConfig(
osVersion: { show: true, labelKey: "iosVersion" }, osVersion: { show: true, labelKey: "iosVersion" },
kernelVersion: { show: false, labelKey: "kernelVersion" }, kernelVersion: { show: false, labelKey: "kernelVersion" },
arch: { show: true, labelKey: "architecture" }, arch: { show: true, labelKey: "architecture" },
deviceModel: { show: true, labelKey: "deviceModel" }, deviceModel: { show: true, labelKey: "deviceModel" }
}, },
android: { android: {
osVersion: { show: true, labelKey: "androidVersion" }, osVersion: { show: true, labelKey: "androidVersion" },
kernelVersion: { show: true, labelKey: "kernelVersion" }, kernelVersion: { show: true, labelKey: "kernelVersion" },
arch: { show: true, labelKey: "architecture" }, arch: { show: true, labelKey: "architecture" },
deviceModel: { show: true, labelKey: "deviceModel" }, deviceModel: { show: true, labelKey: "deviceModel" }
}, },
unknown: { unknown: {
osVersion: { show: true, labelKey: "osVersion" }, osVersion: { show: true, labelKey: "osVersion" },
@@ -133,7 +141,6 @@ function getPlatformFieldConfig(
return configs[normalizedPlatform] || configs.unknown; return configs[normalizedPlatform] || configs.unknown;
} }
export default function GeneralPage() { export default function GeneralPage() {
const { client, updateClient } = useClientContext(); const { client, updateClient } = useClientContext();
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
@@ -423,7 +430,8 @@ export default function GeneralPage() {
{t( {t(
fieldConfig fieldConfig
.osVersion .osVersion
?.labelKey || "osVersion" ?.labelKey ||
"osVersion"
)} )}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
@@ -559,217 +567,231 @@ export default function GeneralPage() {
</SettingsSection> </SettingsSection>
)} )}
{/* Device Security Section */} <SettingsSection>
{build !== "oss" && ( <SettingsSectionHeader>
<SettingsSection> <SettingsSectionTitle>
<SettingsSectionHeader> {t("deviceSecurity")}
<SettingsSectionTitle> </SettingsSectionTitle>
{t("deviceSecurity")} <SettingsSectionDescription>
</SettingsSectionTitle> {t("deviceSecurityDescription")}
<SettingsSectionDescription> </SettingsSectionDescription>
{t("deviceSecurityDescription")} </SettingsSectionHeader>
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
{client.posture && Object.keys(client.posture).length > 0 ? ( <PaidFeaturesAlert />
<> {client.posture &&
{!isPaidUser && <PaidFeaturesAlert />} Object.keys(client.posture).length > 0 ? (
<InfoSections cols={3}> <>
{client.posture.biometricsEnabled !== null && <InfoSections cols={3}>
client.posture.biometricsEnabled !== undefined && ( {client.posture.biometricsEnabled !== null &&
<InfoSection> client.posture.biometricsEnabled !==
<InfoSectionTitle> undefined && (
{t("biometricsEnabled")} <InfoSection>
</InfoSectionTitle> <InfoSectionTitle>
<InfoSectionContent> {t("biometricsEnabled")}
{isPaidUser </InfoSectionTitle>
? formatPostureValue( <InfoSectionContent>
client.posture.biometricsEnabled {isPaidUser
) ? formatPostureValue(
: "-"} client.posture
</InfoSectionContent> .biometricsEnabled
</InfoSection> )
)} : "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.diskEncrypted !== null && {client.posture.diskEncrypted !== null &&
client.posture.diskEncrypted !== undefined && ( client.posture.diskEncrypted !==
<InfoSection> undefined && (
<InfoSectionTitle> <InfoSection>
{t("diskEncrypted")} <InfoSectionTitle>
</InfoSectionTitle> {t("diskEncrypted")}
<InfoSectionContent> </InfoSectionTitle>
{isPaidUser <InfoSectionContent>
? formatPostureValue( {isPaidUser
client.posture.diskEncrypted ? formatPostureValue(
) client.posture
: "-"} .diskEncrypted
</InfoSectionContent> )
</InfoSection> : "-"}
)} </InfoSectionContent>
</InfoSection>
)}
{client.posture.firewallEnabled !== null && {client.posture.firewallEnabled !== null &&
client.posture.firewallEnabled !== undefined && ( client.posture.firewallEnabled !==
<InfoSection> undefined && (
<InfoSectionTitle> <InfoSection>
{t("firewallEnabled")} <InfoSectionTitle>
</InfoSectionTitle> {t("firewallEnabled")}
<InfoSectionContent> </InfoSectionTitle>
{isPaidUser <InfoSectionContent>
? formatPostureValue( {isPaidUser
client.posture.firewallEnabled ? formatPostureValue(
) client.posture
: "-"} .firewallEnabled
</InfoSectionContent> )
</InfoSection> : "-"}
)} </InfoSectionContent>
</InfoSection>
)}
{client.posture.autoUpdatesEnabled !== null && {client.posture.autoUpdatesEnabled !== null &&
client.posture.autoUpdatesEnabled !== undefined && ( client.posture.autoUpdatesEnabled !==
<InfoSection> undefined && (
<InfoSectionTitle> <InfoSection>
{t("autoUpdatesEnabled")} <InfoSectionTitle>
</InfoSectionTitle> {t("autoUpdatesEnabled")}
<InfoSectionContent> </InfoSectionTitle>
{isPaidUser <InfoSectionContent>
? formatPostureValue( {isPaidUser
client.posture.autoUpdatesEnabled ? formatPostureValue(
) client.posture
: "-"} .autoUpdatesEnabled
</InfoSectionContent> )
</InfoSection> : "-"}
)} </InfoSectionContent>
</InfoSection>
)}
{client.posture.tpmAvailable !== null && {client.posture.tpmAvailable !== null &&
client.posture.tpmAvailable !== undefined && ( client.posture.tpmAvailable !==
<InfoSection> undefined && (
<InfoSectionTitle> <InfoSection>
{t("tpmAvailable")} <InfoSectionTitle>
</InfoSectionTitle> {t("tpmAvailable")}
<InfoSectionContent> </InfoSectionTitle>
{isPaidUser <InfoSectionContent>
? formatPostureValue( {isPaidUser
client.posture.tpmAvailable ? formatPostureValue(
) client.posture
: "-"} .tpmAvailable
</InfoSectionContent> )
</InfoSection> : "-"}
)} </InfoSectionContent>
</InfoSection>
)}
{client.posture.windowsAntivirusEnabled !== null && {client.posture.windowsAntivirusEnabled !==
client.posture.windowsAntivirusEnabled !== undefined && ( null &&
<InfoSection> client.posture.windowsAntivirusEnabled !==
<InfoSectionTitle> undefined && (
{t("windowsAntivirusEnabled")} <InfoSection>
</InfoSectionTitle> <InfoSectionTitle>
<InfoSectionContent> {t("windowsAntivirusEnabled")}
{isPaidUser </InfoSectionTitle>
? formatPostureValue( <InfoSectionContent>
client.posture {isPaidUser
.windowsAntivirusEnabled ? formatPostureValue(
) client.posture
: "-"} .windowsAntivirusEnabled
</InfoSectionContent> )
</InfoSection> : "-"}
)} </InfoSectionContent>
</InfoSection>
)}
{client.posture.macosSipEnabled !== null && {client.posture.macosSipEnabled !== null &&
client.posture.macosSipEnabled !== undefined && ( client.posture.macosSipEnabled !==
<InfoSection> undefined && (
<InfoSectionTitle> <InfoSection>
{t("macosSipEnabled")} <InfoSectionTitle>
</InfoSectionTitle> {t("macosSipEnabled")}
<InfoSectionContent> </InfoSectionTitle>
{isPaidUser <InfoSectionContent>
? formatPostureValue( {isPaidUser
client.posture.macosSipEnabled ? formatPostureValue(
) client.posture
: "-"} .macosSipEnabled
</InfoSectionContent> )
</InfoSection> : "-"}
)} </InfoSectionContent>
</InfoSection>
)}
{client.posture.macosGatekeeperEnabled !== null && {client.posture.macosGatekeeperEnabled !==
client.posture.macosGatekeeperEnabled !== null &&
undefined && ( client.posture.macosGatekeeperEnabled !==
<InfoSection> undefined && (
<InfoSectionTitle> <InfoSection>
{t("macosGatekeeperEnabled")} <InfoSectionTitle>
</InfoSectionTitle> {t("macosGatekeeperEnabled")}
<InfoSectionContent> </InfoSectionTitle>
{isPaidUser <InfoSectionContent>
? formatPostureValue( {isPaidUser
client.posture ? formatPostureValue(
.macosGatekeeperEnabled client.posture
) .macosGatekeeperEnabled
: "-"} )
</InfoSectionContent> : "-"}
</InfoSection> </InfoSectionContent>
)} </InfoSection>
)}
{client.posture.macosFirewallStealthMode !== null && {client.posture.macosFirewallStealthMode !==
client.posture.macosFirewallStealthMode !== null &&
undefined && ( client.posture.macosFirewallStealthMode !==
<InfoSection> undefined && (
<InfoSectionTitle> <InfoSection>
{t("macosFirewallStealthMode")} <InfoSectionTitle>
</InfoSectionTitle> {t("macosFirewallStealthMode")}
<InfoSectionContent> </InfoSectionTitle>
{isPaidUser <InfoSectionContent>
? formatPostureValue( {isPaidUser
client.posture ? formatPostureValue(
.macosFirewallStealthMode client.posture
) .macosFirewallStealthMode
: "-"} )
</InfoSectionContent> : "-"}
</InfoSection> </InfoSectionContent>
)} </InfoSection>
)}
{client.posture.linuxAppArmorEnabled !== null && {client.posture.linuxAppArmorEnabled !== null &&
client.posture.linuxAppArmorEnabled !== client.posture.linuxAppArmorEnabled !==
undefined && ( undefined && (
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t("linuxAppArmorEnabled")} {t("linuxAppArmorEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser {isPaidUser
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.linuxAppArmorEnabled .linuxAppArmorEnabled
) )
: "-"} : "-"}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
)} )}
{client.posture.linuxSELinuxEnabled !== null && {client.posture.linuxSELinuxEnabled !== null &&
client.posture.linuxSELinuxEnabled !== client.posture.linuxSELinuxEnabled !==
undefined && ( undefined && (
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t("linuxSELinuxEnabled")} {t("linuxSELinuxEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser {isPaidUser
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.linuxSELinuxEnabled .linuxSELinuxEnabled
) )
: "-"} : "-"}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
)} )}
</InfoSections> </InfoSections>
</> </>
) : ( ) : (
<div className="text-muted-foreground"> <div className="text-muted-foreground">
{t("noData")} {t("noData")}
</div> </div>
)} )}
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
)}
</SettingsContainer> </SettingsContainer>
); );
} }

View File

@@ -20,11 +20,6 @@ export interface AuthPageProps {
export default async function AuthPage(props: AuthPageProps) { export default async function AuthPage(props: AuthPageProps) {
const orgId = (await props.params).orgId; 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; let subscriptionStatus: GetOrgTierResponse | null = null;
try { try {
const subRes = await getCachedSubscription(orgId); const subRes = await getCachedSubscription(orgId);

View File

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

View File

@@ -3,12 +3,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { import { useState, useRef, useActionState, type ComponentRef } from "react";
useState,
useRef,
useActionState,
type ComponentRef
} from "react";
import { import {
Form, Form,
FormControl, FormControl,
@@ -110,7 +105,7 @@ export default function SecurityPage() {
return ( return (
<SettingsContainer> <SettingsContainer>
<LogRetentionSectionForm org={org.org} /> <LogRetentionSectionForm org={org.org} />
{build !== "oss" && <SecuritySettingsSectionForm org={org.org} />} <SecuritySettingsSectionForm org={org.org} />
</SettingsContainer> </SettingsContainer>
); );
} }
@@ -243,144 +238,120 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
)} )}
/> />
{build !== "oss" && ( <PaidFeaturesAlert />
<>
<PaidFeaturesAlert />
<FormField <FormField
control={form.control} control={form.control}
name="settingsLogRetentionDaysAccess" name="settingsLogRetentionDaysAccess"
render={({ field }) => { render={({ field }) => {
const isDisabled = !isPaidUser; const isDisabled = !isPaidUser;
return ( return (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t( {t("logRetentionAccessLabel")}
"logRetentionAccessLabel" </FormLabel>
)} <FormControl>
</FormLabel> <Select
<FormControl> value={field.value.toString()}
<Select onValueChange={(value) => {
value={field.value.toString()} if (!isDisabled) {
onValueChange={( field.onChange(
value parseInt(
) => { value,
if ( 10
!isDisabled )
) { );
field.onChange( }
parseInt( }}
value, disabled={isDisabled}
10 >
) <SelectTrigger>
); <SelectValue
} placeholder={t(
}} "selectLogRetention"
disabled={ )}
isDisabled />
} </SelectTrigger>
> <SelectContent>
<SelectTrigger> {LOG_RETENTION_OPTIONS.map(
<SelectValue (option) => (
placeholder={t( <SelectItem
"selectLogRetention" key={
option.value
}
value={option.value.toString()}
>
{t(
option.label
)} )}
/> </SelectItem>
</SelectTrigger> )
<SelectContent>
{LOG_RETENTION_OPTIONS.map(
(
option
) => (
<SelectItem
key={
option.value
}
value={option.value.toString()}
>
{t(
option.label
)}
</SelectItem>
)
)}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="settingsLogRetentionDaysAction"
render={({ field }) => {
const isDisabled = !isPaidUser;
return (
<FormItem>
<FormLabel>
{t(
"logRetentionActionLabel"
)} )}
</FormLabel> </SelectContent>
<FormControl> </Select>
<Select </FormControl>
value={field.value.toString()} <FormMessage />
onValueChange={( </FormItem>
value );
) => { }}
if ( />
!isDisabled <FormField
) { control={form.control}
field.onChange( name="settingsLogRetentionDaysAction"
parseInt( render={({ field }) => {
value, const isDisabled = !isPaidUser;
10
) return (
); <FormItem>
} <FormLabel>
}} {t("logRetentionActionLabel")}
disabled={ </FormLabel>
isDisabled <FormControl>
} <Select
> value={field.value.toString()}
<SelectTrigger> onValueChange={(value) => {
<SelectValue if (!isDisabled) {
placeholder={t( field.onChange(
"selectLogRetention" parseInt(
value,
10
)
);
}
}}
disabled={isDisabled}
>
<SelectTrigger>
<SelectValue
placeholder={t(
"selectLogRetention"
)}
/>
</SelectTrigger>
<SelectContent>
{LOG_RETENTION_OPTIONS.map(
(option) => (
<SelectItem
key={
option.value
}
value={option.value.toString()}
>
{t(
option.label
)} )}
/> </SelectItem>
</SelectTrigger> )
<SelectContent> )}
{LOG_RETENTION_OPTIONS.map( </SelectContent>
( </Select>
option </FormControl>
) => ( <FormMessage />
<SelectItem </FormItem>
key={ );
option.value }}
} />
value={option.value.toString()}
>
{t(
option.label
)}
</SelectItem>
)
)}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</>
)}
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
@@ -740,7 +711,7 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
type="submit" type="submit"
form="security-settings-section-form" form="security-settings-section-form"
loading={loadingSave} loading={loadingSave}
disabled={loadingSave} disabled={loadingSave || !isPaidUser}
> >
{t("saveSettings")} {t("saveSettings")}
</Button> </Button>

View File

@@ -20,6 +20,7 @@ import { Alert, AlertDescription } from "@app/components/ui/alert";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
import axios from "axios"; import axios from "axios";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
export default function GeneralPage() { export default function GeneralPage() {
const router = useRouter(); const router = useRouter();
@@ -209,7 +210,8 @@ export default function GeneralPage() {
console.log("Date range changed:", { startDate, endDate, page, size }); console.log("Date range changed:", { startDate, endDate, page, size });
if ( if (
(build == "saas" && !subscription?.subscribed) || (build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked()) (build == "enterprise" && !isUnlocked()) ||
build === "oss"
) { ) {
console.log( console.log(
"Access denied: subscription inactive or license locked" "Access denied: subscription inactive or license locked"
@@ -611,21 +613,7 @@ export default function GeneralPage() {
description={t("accessLogsDescription")} description={t("accessLogsDescription")}
/> />
{build == "saas" && !subscription?.subscribed ? ( <PaidFeaturesAlert />
<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}
<LogDataTable <LogDataTable
columns={columns} columns={columns}
@@ -656,7 +644,8 @@ export default function GeneralPage() {
renderExpandedRow={renderExpandedRow} renderExpandedRow={renderExpandedRow}
disabled={ disabled={
(build == "saas" && !subscription?.subscribed) || (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 { ColumnFilter } from "@app/components/ColumnFilter";
import { DateTimeValue } from "@app/components/DateTimePicker"; import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable"; import { LogDataTable } from "@app/components/LogDataTable";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Alert, AlertDescription } from "@app/components/ui/alert";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -92,6 +93,9 @@ export default function GeneralPage() {
// Trigger search with default values on component mount // Trigger search with default values on component mount
useEffect(() => { useEffect(() => {
if (build === "oss") {
return;
}
const defaultRange = getDefaultDateRange(); const defaultRange = getDefaultDateRange();
queryDateTime( queryDateTime(
defaultRange.startDate, defaultRange.startDate,
@@ -461,21 +465,7 @@ export default function GeneralPage() {
description={t("actionLogsDescription")} description={t("actionLogsDescription")}
/> />
{build == "saas" && !subscription?.subscribed ? ( <PaidFeaturesAlert />
<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}
<LogDataTable <LogDataTable
columns={columns} columns={columns}
@@ -508,7 +498,8 @@ export default function GeneralPage() {
renderExpandedRow={renderExpandedRow} renderExpandedRow={renderExpandedRow}
disabled={ disabled={
(build == "saas" && !subscription?.subscribed) || (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 { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState, useTransition } from "react"; import { useEffect, useState, useTransition } from "react";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { build } from "@server/build";
export default function GeneralPage() { export default function GeneralPage() {
const router = useRouter(); const router = useRouter();
@@ -110,6 +111,9 @@ export default function GeneralPage() {
// Trigger search with default values on component mount // Trigger search with default values on component mount
useEffect(() => { useEffect(() => {
if (build === "oss") {
return;
}
const defaultRange = getDefaultDateRange(); const defaultRange = getDefaultDateRange();
queryDateTime( queryDateTime(
defaultRange.startDate, defaultRange.startDate,

View File

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

View File

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

View File

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

View File

@@ -30,6 +30,7 @@ import {
import { Separator } from "./ui/separator"; import { Separator } from "./ui/separator";
import { InfoPopup } from "./ui/info-popup"; import { InfoPopup } from "./ui/info-popup";
import { ApprovalsEmptyState } from "./ApprovalsEmptyState"; import { ApprovalsEmptyState } from "./ApprovalsEmptyState";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
export type ApprovalFeedProps = { export type ApprovalFeedProps = {
orgId: string; orgId: string;
@@ -50,9 +51,12 @@ export function ApprovalFeed({
Object.fromEntries(searchParams.entries()) Object.fromEntries(searchParams.entries())
); );
const { data, isFetching, refetch } = useQuery( const { isPaidUser } = usePaidStatus();
approvalQueries.listApprovals(orgId, filters)
); const { data, isFetching, refetch } = useQuery({
...approvalQueries.listApprovals(orgId, filters),
enabled: isPaidUser
});
const approvals = data?.approvals ?? []; const approvals = data?.approvals ?? [];
@@ -209,19 +213,19 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
&nbsp; &nbsp;
{approval.type === "user_device" && ( {approval.type === "user_device" && (
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
{approval.deviceName ? ( {approval.deviceName ? (
<> <>
{t("requestingNewDeviceApproval")}:{" "} {t("requestingNewDeviceApproval")}:{" "}
{approval.niceId ? ( {approval.niceId ? (
<Link <Link
href={`/${orgId}/settings/clients/user/${approval.niceId}/general`} href={`/${orgId}/settings/clients/user/${approval.niceId}/general`}
className="text-primary hover:underline cursor-pointer" className="text-primary hover:underline cursor-pointer"
> >
{approval.deviceName} {approval.deviceName}
</Link> </Link>
) : ( ) : (
<span>{approval.deviceName}</span> <span>{approval.deviceName}</span>
)} )}
{approval.fingerprint && ( {approval.fingerprint && (
<InfoPopup> <InfoPopup>
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">
@@ -229,7 +233,10 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
{t("deviceInformation")} {t("deviceInformation")}
</div> </div>
<div className="text-muted-foreground whitespace-pre-line"> <div className="text-muted-foreground whitespace-pre-line">
{formatFingerprintInfo(approval.fingerprint, t)} {formatFingerprintInfo(
approval.fingerprint,
t
)}
</div> </div>
</div> </div>
</InfoPopup> </InfoPopup>

View File

@@ -160,56 +160,51 @@ export default function CreateRoleForm({
</FormItem> </FormItem>
)} )}
/> />
{build !== "oss" && (
<div>
<PaidFeaturesAlert />
<FormField <PaidFeaturesAlert />
control={form.control}
name="requireDeviceApproval" <FormField
render={({ field }) => ( control={form.control}
<FormItem className="my-2"> name="requireDeviceApproval"
<FormControl> render={({ field }) => (
<CheckboxWithLabel <FormItem className="my-2">
{...field} <FormControl>
disabled={ <CheckboxWithLabel
!isPaidUser {...field}
} disabled={!isPaidUser}
value="on" value="on"
checked={form.watch( checked={form.watch(
"requireDeviceApproval" "requireDeviceApproval"
)} )}
onCheckedChange={( onCheckedChange={(
checked
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
checked checked
) => { );
if ( }
checked !== }}
"indeterminate" label={t(
) { "requireDeviceApproval"
form.setValue( )}
"requireDeviceApproval", />
checked </FormControl>
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
<FormDescription> <FormDescription>
{t( {t(
"requireDeviceApprovalDescription" "requireDeviceApprovalDescription"
)} )}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div>
)}
</form> </form>
</Form> </Form>
</CredenzaBody> </CredenzaBody>

View File

@@ -168,56 +168,50 @@ export default function EditRoleForm({
</FormItem> </FormItem>
)} )}
/> />
{build !== "oss" && ( <PaidFeaturesAlert />
<div>
<PaidFeaturesAlert />
<FormField <FormField
control={form.control} control={form.control}
name="requireDeviceApproval" name="requireDeviceApproval"
render={({ field }) => ( render={({ field }) => (
<FormItem className="my-2"> <FormItem className="my-2">
<FormControl> <FormControl>
<CheckboxWithLabel <CheckboxWithLabel
{...field} {...field}
disabled={ disabled={!isPaidUser}
!isPaidUser value="on"
} checked={form.watch(
value="on" "requireDeviceApproval"
checked={form.watch( )}
"requireDeviceApproval" onCheckedChange={(
)} checked
onCheckedChange={( ) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
checked checked
) => { );
if ( }
checked !== }}
"indeterminate" label={t(
) { "requireDeviceApproval"
form.setValue( )}
"requireDeviceApproval", />
checked </FormControl>
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
<FormDescription> <FormDescription>
{t( {t(
"requireDeviceApprovalDescription" "requireDeviceApprovalDescription"
)} )}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div>
)}
</form> </form>
</Form> </Form>
</CredenzaBody> </CredenzaBody>

View File

@@ -1,8 +1,16 @@
"use client"; "use client";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { Card, CardContent } from "@app/components/ui/card";
import { build } from "@server/build"; import { build } from "@server/build";
import { useTranslations } from "next-intl";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; 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() { export function PaidFeaturesAlert() {
const t = useTranslations(); const t = useTranslations();
@@ -10,19 +18,50 @@ export function PaidFeaturesAlert() {
return ( return (
<> <>
{build === "saas" && !hasSaasSubscription ? ( {build === "saas" && !hasSaasSubscription ? (
<Alert variant="info" className="mb-6"> <Card className={bannerClassName}>
<AlertDescription> <CardContent className={bannerContentClassName}>
{t("subscriptionRequiredToUse")} <div className={bannerRowClassName}>
</AlertDescription> <KeyRound className="size-4 shrink-0 text-primary" />
</Alert> <span>{t("subscriptionRequiredToUse")}</span>
</div>
</CardContent>
</Card>
) : null} ) : null}
{build === "enterprise" && !hasEnterpriseLicense ? ( {build === "enterprise" && !hasEnterpriseLicense ? (
<Alert variant="info" className="mb-6"> <Card className={bannerClassName}>
<AlertDescription> <CardContent className={bannerContentClassName}>
{t("licenseRequiredToUse")} <div className={bannerRowClassName}>
</AlertDescription> <KeyRound className="size-4 shrink-0 text-primary" />
</Alert> <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} ) : null}
</> </>
); );

View File

@@ -190,7 +190,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
const approvalsRes = await api.get<{ const approvalsRes = await api.get<{
data: { approvals: Array<{ approvalId: number; clientId: number }> }; data: { approvals: Array<{ approvalId: number; clientId: number }> };
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`); }>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
const approval = approvalsRes.data.data.approvals[0]; const approval = approvalsRes.data.data.approvals[0];
if (!approval) { if (!approval) {
@@ -232,7 +232,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
const approvalsRes = await api.get<{ const approvalsRes = await api.get<{
data: { approvals: Array<{ approvalId: number; clientId: number }> }; data: { approvals: Array<{ approvalId: number; clientId: number }> };
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`); }>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
const approval = approvalsRes.data.data.approvals[0]; const approval = approvalsRes.data.data.approvals[0];
if (!approval) { if (!approval) {
@@ -548,7 +548,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
{clientRow.approvalState === "pending" && build !== "oss" && ( {clientRow.approvalState === "pending" && (
<> <>
<DropdownMenuItem <DropdownMenuItem
onClick={() => approveDevice(clientRow)} 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; return allOptions;
}, [t]); }, [t]);
const statusFilterDefaultValues = useMemo(() => { const statusFilterDefaultValues = useMemo(() => {
if (build === "oss") {
return ["active"];
}
return ["active", "pending"]; return ["active", "pending"];
}, []); }, []);

View File

@@ -2,29 +2,6 @@ import { NextRequest, NextResponse } from "next/server";
import { build } from "@server/build"; import { build } from "@server/build";
export function middleware(request: NextRequest) { 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(); return NextResponse.next();
} }