refactor is licensed and subscribed util functions

This commit is contained in:
miloschwartz
2026-02-09 16:57:41 -08:00
parent e6464929ff
commit dff45748bd
25 changed files with 707 additions and 574 deletions

View File

@@ -107,6 +107,11 @@ export class Config {
process.env.MAXMIND_ASN_PATH = parsedConfig.server.maxmind_asn_path;
}
process.env.DISABLE_ENTERPRISE_FEATURES = parsedConfig.flags
?.disable_enterprise_features
? "true"
: "false";
this.rawConfig = parsedConfig;
}

View File

@@ -1,3 +1,6 @@
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
export async function isLicensedOrSubscribed(
orgId: string,
tiers: string[]
): Promise<boolean> {
return false;
}
}

View File

@@ -1,3 +1,6 @@
export async function isSubscribed(orgId: string): Promise<boolean> {
export async function isSubscribed(
orgId: string,
tiers: string[]
): Promise<boolean> {
return false;
}

View File

@@ -331,7 +331,8 @@ export const configSchema = z
disable_local_sites: z.boolean().optional(),
disable_basic_wireguard_sites: z.boolean().optional(),
disable_config_managed_domains: z.boolean().optional(),
disable_product_help_banners: z.boolean().optional()
disable_product_help_banners: z.boolean().optional(),
disable_enterprise_features: z.boolean().optional()
})
.optional(),
dns: z

View File

@@ -78,6 +78,8 @@ export async function checkOrgAccessPolicy(
}
}
// TODO: check that the org is subscribed
// get the needed data
if (!props.org) {

View File

@@ -13,16 +13,18 @@
import { build } from "@server/build";
import license from "#private/license/license";
import { getOrgTierData } from "#private/lib/billing";
import { isSubscribed } from "#private/lib/isSubscribed";
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
export async function isLicensedOrSubscribed(
orgId: string,
tiers: string[]
): Promise<boolean> {
if (build === "enterprise") {
return await license.isUnlocked();
}
if (build === "saas") {
const { tier, active } = await getOrgTierData(orgId);
return (tier == "tier1" || tier == "tier2" || tier == "tier3") && active;
return isSubscribed(orgId, tiers);
}
return false;

View File

@@ -14,10 +14,14 @@
import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
export async function isSubscribed(orgId: string): Promise<boolean> {
export async function isSubscribed(
orgId: string,
tiers: string[]
): Promise<boolean> {
if (build === "saas") {
const { tier, active } = await getOrgTierData(orgId);
return (tier == "tier1" || tier == "tier2" || tier == "tier3") && active;
const isTier = (tier && tiers.includes(tier)) || false;
return active && isTier;
}
return false;

View File

@@ -17,44 +17,51 @@ import HttpCode from "@server/types/HttpCode";
import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
export async function verifyValidSubscription(
req: Request,
res: Response,
next: NextFunction
) {
try {
if (build != "saas") {
export function verifyValidSubscription(tiers: string[]) {
return async function (
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
if (build != "saas") {
return next();
}
const orgId =
req.params.orgId ||
req.body.orgId ||
req.query.orgId ||
req.userOrgId;
if (!orgId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization ID is required to verify subscription"
)
);
}
const { tier, active } = await getOrgTierData(orgId);
const isTier = tiers.includes(tier || "");
if (!isTier || !active) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Organization does not have an active subscription"
)
);
}
return next();
}
const orgId = req.params.orgId || req.body.orgId || req.query.orgId || req.userOrgId;
if (!orgId) {
} catch (e) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization ID is required to verify subscription"
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying subscription"
)
);
}
const { tier, active } = await getOrgTierData(orgId);
if ((tier == "tier1" || tier == "tier2" || tier == "tier3") && active) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Organization does not have an active subscription"
)
);
}
return next();
} catch (e) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying subscription"
)
);
}
};
}

View File

@@ -86,7 +86,7 @@ authenticated.put(
authenticated.post(
"/org/:orgId/idp/:idpId/oidc",
verifyValidLicense,
verifyValidSubscription,
verifyValidSubscription(),
verifyOrgAccess,
verifyIdpAccess,
verifyUserHasAction(ActionsEnum.updateIdp),

View File

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

View File

@@ -183,27 +183,29 @@ export default function CredentialsPage() {
</Alert>
)}
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
variant="outline"
onClick={() => {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("regenerateCredentialsButton")}
</Button>
<Button
onClick={() => {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("clientRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
{!env.flags.disableEnterpriseFeatures && (
<SettingsSectionFooter>
<Button
variant="outline"
onClick={() => {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("regenerateCredentialsButton")}
</Button>
<Button
onClick={() => {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("clientRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
)}
</SettingsSection>
<OlmInstallCommands

View File

@@ -152,6 +152,7 @@ export default function GeneralPage() {
const [approvalId, setApprovalId] = useState<number | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const [, startTransition] = useTransition();
const { env } = useEnvContext();
const showApprovalFeatures = build !== "oss" && isPaidUser;
@@ -567,231 +568,246 @@ export default function GeneralPage() {
</SettingsSection>
)}
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("deviceSecurity")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("deviceSecurityDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
{!env.flags.disableEnterpriseFeatures && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("deviceSecurity")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("deviceSecurityDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<PaidFeaturesAlert />
{client.posture &&
Object.keys(client.posture).length > 0 ? (
<>
<InfoSections cols={3}>
{client.posture.biometricsEnabled !== null &&
client.posture.biometricsEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("biometricsEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.biometricsEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
<SettingsSectionBody>
<PaidFeaturesAlert />
{client.posture &&
Object.keys(client.posture).length > 0 ? (
<>
<InfoSections cols={3}>
{client.posture.biometricsEnabled !==
null &&
client.posture.biometricsEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("biometricsEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.biometricsEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.diskEncrypted !== null &&
client.posture.diskEncrypted !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("diskEncrypted")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.diskEncrypted
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.diskEncrypted !== null &&
client.posture.diskEncrypted !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("diskEncrypted")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.diskEncrypted
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.firewallEnabled !== null &&
client.posture.firewallEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("firewallEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.firewallEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.firewallEnabled !== null &&
client.posture.firewallEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("firewallEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.firewallEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.autoUpdatesEnabled !== null &&
client.posture.autoUpdatesEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("autoUpdatesEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.autoUpdatesEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.autoUpdatesEnabled !==
null &&
client.posture.autoUpdatesEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("autoUpdatesEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.autoUpdatesEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.tpmAvailable !== null &&
client.posture.tpmAvailable !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("tpmAvailable")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.tpmAvailable
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.tpmAvailable !== null &&
client.posture.tpmAvailable !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("tpmAvailable")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.tpmAvailable
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.windowsAntivirusEnabled !==
null &&
client.posture.windowsAntivirusEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("windowsAntivirusEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.windowsAntivirusEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.windowsAntivirusEnabled !==
null &&
client.posture
.windowsAntivirusEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t(
"windowsAntivirusEnabled"
)}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.windowsAntivirusEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosSipEnabled !== null &&
client.posture.macosSipEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("macosSipEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.macosSipEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosSipEnabled !== null &&
client.posture.macosSipEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("macosSipEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.macosSipEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosGatekeeperEnabled !==
null &&
client.posture.macosGatekeeperEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("macosGatekeeperEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.macosGatekeeperEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosGatekeeperEnabled !==
null &&
client.posture
.macosGatekeeperEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t(
"macosGatekeeperEnabled"
)}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.macosGatekeeperEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosFirewallStealthMode !==
null &&
client.posture.macosFirewallStealthMode !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("macosFirewallStealthMode")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.macosFirewallStealthMode
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosFirewallStealthMode !==
null &&
client.posture
.macosFirewallStealthMode !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t(
"macosFirewallStealthMode"
)}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.macosFirewallStealthMode
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.linuxAppArmorEnabled !== null &&
client.posture.linuxAppArmorEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("linuxAppArmorEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.linuxAppArmorEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.linuxAppArmorEnabled !==
null &&
client.posture.linuxAppArmorEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("linuxAppArmorEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.linuxAppArmorEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.linuxSELinuxEnabled !== null &&
client.posture.linuxSELinuxEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("linuxSELinuxEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.linuxSELinuxEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
</InfoSections>
</>
) : (
<div className="text-muted-foreground">
{t("noData")}
</div>
)}
</SettingsSectionBody>
</SettingsSection>
{client.posture.linuxSELinuxEnabled !==
null &&
client.posture.linuxSELinuxEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("linuxSELinuxEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.linuxSELinuxEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
</InfoSections>
</>
) : (
<div className="text-muted-foreground">
{t("noData")}
</div>
)}
</SettingsSectionBody>
</SettingsSection>
)}
</SettingsContainer>
);
}

View File

@@ -10,6 +10,7 @@ import { getTranslations } from "next-intl/server";
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser";
import { build } from "@server/build";
import { pullEnv } from "@app/lib/pullEnv";
type GeneralSettingsProps = {
children: React.ReactNode;
@@ -23,6 +24,7 @@ export default async function GeneralSettingsPage({
const { orgId } = await params;
const user = await verifySession();
const env = pullEnv();
if (!user) {
redirect(`/`);
@@ -56,10 +58,15 @@ export default async function GeneralSettingsPage({
title: t("security"),
href: `/{orgId}/settings/general/security`
},
{
title: t("authPage"),
href: `/{orgId}/settings/general/auth-page`
}
// PaidFeaturesAlert
...(!env.flags.disableEnterpriseFeatures
? [
{
title: t("authPage"),
href: `/{orgId}/settings/general/auth-page`
}
]
: [])
];
return (

View File

@@ -102,10 +102,13 @@ type SectionFormProps = {
export default function SecurityPage() {
const { org } = useOrgContext();
const { env } = useEnvContext();
return (
<SettingsContainer>
<LogRetentionSectionForm org={org.org} />
<SecuritySettingsSectionForm org={org.org} />
{!env.flags.disableEnterpriseFeatures && (
<SecuritySettingsSectionForm org={org.org} />
)}
</SettingsContainer>
);
}
@@ -135,7 +138,8 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
const { isPaidUser, hasSaasSubscription } = usePaidStatus();
const [, formAction, loadingSave] = useActionState(performSave, null);
const api = createApiClient(useEnvContext());
const { env } = useEnvContext();
const api = createApiClient({ env });
async function performSave() {
const isValid = await form.trigger();
@@ -238,120 +242,144 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
)}
/>
<PaidFeaturesAlert />
{!env.flags.disableEnterpriseFeatures && (
<>
<PaidFeaturesAlert />
<FormField
control={form.control}
name="settingsLogRetentionDaysAccess"
render={({ field }) => {
const isDisabled = !isPaidUser;
<FormField
control={form.control}
name="settingsLogRetentionDaysAccess"
render={({ field }) => {
const isDisabled = !isPaidUser;
return (
<FormItem>
<FormLabel>
{t("logRetentionAccessLabel")}
</FormLabel>
<FormControl>
<Select
value={field.value.toString()}
onValueChange={(value) => {
if (!isDisabled) {
field.onChange(
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>
)
return (
<FormItem>
<FormLabel>
{t(
"logRetentionAccessLabel"
)}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="settingsLogRetentionDaysAction"
render={({ field }) => {
const isDisabled = !isPaidUser;
return (
<FormItem>
<FormLabel>
{t("logRetentionActionLabel")}
</FormLabel>
<FormControl>
<Select
value={field.value.toString()}
onValueChange={(value) => {
if (!isDisabled) {
field.onChange(
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
</FormLabel>
<FormControl>
<Select
value={field.value.toString()}
onValueChange={(
value
) => {
if (
!isDisabled
) {
field.onChange(
parseInt(
value,
10
)
);
}
}}
disabled={
isDisabled
}
>
<SelectTrigger>
<SelectValue
placeholder={t(
"selectLogRetention"
)}
</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"
)}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</FormLabel>
<FormControl>
<Select
value={field.value.toString()}
onValueChange={(
value
) => {
if (
!isDisabled
) {
field.onChange(
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>
)
)}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</>
)}
</form>
</Form>
</SettingsSectionForm>

View File

@@ -163,7 +163,9 @@ function MaintenanceSectionForm({
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss";
return (
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
);
};
if (!resource.http) {
@@ -189,13 +191,14 @@ function MaintenanceSectionForm({
className="space-y-4"
id="maintenance-settings-form"
>
<PaidFeaturesAlert></PaidFeaturesAlert>
<PaidFeaturesAlert />
<FormField
control={maintenanceForm.control}
name="maintenanceModeEnabled"
render={({ field }) => {
const isDisabled =
isSecurityFeatureDisabled() || resource.http === false;
isSecurityFeatureDisabled() ||
resource.http === false;
return (
<FormItem>
@@ -415,7 +418,7 @@ function MaintenanceSectionForm({
<Button
type="submit"
loading={maintenanceSaveLoading}
disabled={maintenanceSaveLoading || !isPaidUser }
disabled={maintenanceSaveLoading || !isPaidUser}
form="maintenance-settings-form"
>
{t("saveSettings")}
@@ -741,10 +744,12 @@ export default function GeneralForm() {
</SettingsSectionFooter>
</SettingsSection>
<MaintenanceSectionForm
resource={resource}
updateResource={updateResource}
/>
{!env.flags.disableEnterpriseFeatures && (
<MaintenanceSectionForm
resource={resource}
updateResource={updateResource}
/>
)}
</SettingsContainer>
<Credenza

View File

@@ -271,27 +271,29 @@ export default function CredentialsPage() {
</Alert>
)}
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
variant="outline"
onClick={() => {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("regenerateCredentialsButton")}
</Button>
<Button
onClick={() => {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("siteRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
{!env.flags.disableEnterpriseFeatures && (
<SettingsSectionFooter>
<Button
variant="outline"
onClick={() => {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("regenerateCredentialsButton")}
</Button>
<Button
onClick={() => {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("siteRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
)}
</SettingsSection>
<NewtSiteInstallCommands
@@ -383,14 +385,16 @@ export default function CredentialsPage() {
</>
)}
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
onClick={() => setModalOpen(true)}
disabled={isSecurityFeatureDisabled()}
>
{t("siteRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
{!env.flags.disableEnterpriseFeatures && (
<SettingsSectionFooter>
<Button
onClick={() => setModalOpen(true)}
disabled={isSecurityFeatureDisabled()}
>
{t("siteRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
)}
</SettingsSection>
)}
</SettingsContainer>

View File

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

View File

@@ -51,6 +51,7 @@ export default function CreateRoleForm({
const { org } = useOrgContext();
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const { env } = useEnvContext();
const formSchema = z.object({
name: z
@@ -161,50 +162,56 @@ export default function CreateRoleForm({
)}
/>
<PaidFeaturesAlert />
{!env.flags.disableEnterpriseFeatures && (
<>
<PaidFeaturesAlert />
<FormField
control={form.control}
name="requireDeviceApproval"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
disabled={!isPaidUser}
value="on"
checked={form.watch(
"requireDeviceApproval"
)}
onCheckedChange={(
checked
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
<FormField
control={form.control}
name="requireDeviceApproval"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
disabled={
!isPaidUser
}
value="on"
checked={form.watch(
"requireDeviceApproval"
)}
onCheckedChange={(
checked
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
checked
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
<FormDescription>
{t(
"requireDeviceApprovalDescription"
)}
</FormDescription>
<FormDescription>
{t(
"requireDeviceApprovalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
</>
)}
</form>
</Form>
</CredenzaBody>

View File

@@ -59,6 +59,7 @@ export default function EditRoleForm({
const { org } = useOrgContext();
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const { env } = useEnvContext();
const formSchema = z.object({
name: z
@@ -168,50 +169,57 @@ export default function EditRoleForm({
</FormItem>
)}
/>
<PaidFeaturesAlert />
<FormField
control={form.control}
name="requireDeviceApproval"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
disabled={!isPaidUser}
value="on"
checked={form.watch(
"requireDeviceApproval"
)}
onCheckedChange={(
checked
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
{!env.flags.disableEnterpriseFeatures && (
<>
<PaidFeaturesAlert />
<FormField
control={form.control}
name="requireDeviceApproval"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
disabled={
!isPaidUser
}
value="on"
checked={form.watch(
"requireDeviceApproval"
)}
onCheckedChange={(
checked
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
checked
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
<FormDescription>
{t(
"requireDeviceApprovalDescription"
)}
</FormDescription>
<FormDescription>
{t(
"requireDeviceApprovalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
</>
)}
</form>
</Form>
</CredenzaBody>

View File

@@ -5,6 +5,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { ExternalLink, KeyRound, Sparkles } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useEnvContext } from "@app/hooks/useEnvContext";
const bannerClassName =
"mb-6 border-primary/30 bg-linear-to-br from-primary/10 via-background to-background overflow-hidden";
@@ -15,6 +16,12 @@ const bannerRowClassName =
export function PaidFeaturesAlert() {
const t = useTranslations();
const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus();
const { env } = useEnvContext();
if (env.flags.disableEnterpriseFeatures) {
return null;
}
return (
<>
{build === "saas" && !hasSaasSubscription ? (

View File

@@ -4,7 +4,6 @@ import { createContext } from "react";
type SubscriptionStatusContextType = {
subscriptionStatus: GetOrgSubscriptionResponse | null;
updateSubscriptionStatus: (updatedSite: GetOrgSubscriptionResponse) => void;
isActive: () => boolean;
getTier: () => { tier: string | null; active: boolean };
isSubscribed: () => boolean;
subscribed: boolean;

View File

@@ -8,14 +8,29 @@ export function usePaidStatus() {
// Check if features are disabled due to licensing/subscription
const hasEnterpriseLicense = build === "enterprise" && isUnlocked();
const hasSaasSubscription =
build === "saas" &&
subscription?.isSubscribed() &&
subscription.isActive();
const tierData = subscription?.getTier();
const hasSaasSubscription = build === "saas" && tierData?.active;
function isPaidUser(tiers: string[]): boolean {
if (hasEnterpriseLicense) {
return true;
}
if (
hasSaasSubscription &&
tierData?.tier &&
tiers.includes(tierData.tier)
) {
return true;
}
return false;
}
return {
hasEnterpriseLicense,
hasSaasSubscription,
isPaidUser: hasEnterpriseLicense || hasSaasSubscription
isPaidUser,
subscriptionTier: tierData?.tier
};
}

View File

@@ -65,7 +65,11 @@ export function pullEnv(): Env {
? true
: false,
useOrgOnlyIdp:
process.env.USE_ORG_ONLY_IDP === "true" ? true : false
process.env.USE_ORG_ONLY_IDP === "true" ? true : false,
disableEnterpriseFeatures:
process.env.DISABLE_ENTERPRISE_FEATURES === "true"
? true
: false
},
branding: {

View File

@@ -35,6 +35,7 @@ export type Env = {
usePangolinDns: boolean;
disableProductHelpBanners: boolean;
useOrgOnlyIdp: boolean;
disableEnterpriseFeatures: boolean;
};
branding: {
appName?: string;

View File

@@ -31,16 +31,6 @@ export function SubscriptionStatusProvider({
});
};
const isActive = () => {
if (subscriptionStatus?.subscriptions) {
// Check if any subscription is active
return subscriptionStatus.subscriptions.some(
(sub) => sub.subscription?.status === "active"
);
}
return false;
};
const getTier = () => {
if (subscriptionStatus?.subscriptions) {
// Iterate through all subscriptions
@@ -65,9 +55,6 @@ export function SubscriptionStatusProvider({
};
const isSubscribed = () => {
if (build === "enterprise") {
return true;
}
const { tier, active } = getTier();
return (
(tier == "tier1" || tier == "tier2" || tier == "tier3") &&
@@ -82,7 +69,6 @@ export function SubscriptionStatusProvider({
value={{
subscriptionStatus: subscriptionStatusState,
updateSubscriptionStatus,
isActive,
getTier,
isSubscribed,
subscribed