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.MAXMIND_ASN_PATH = parsedConfig.server.maxmind_asn_path;
} }
process.env.DISABLE_ENTERPRISE_FEATURES = parsedConfig.flags
?.disable_enterprise_features
? "true"
: "false";
this.rawConfig = parsedConfig; 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; 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; return false;
} }

View File

@@ -331,7 +331,8 @@ export const configSchema = z
disable_local_sites: z.boolean().optional(), disable_local_sites: z.boolean().optional(),
disable_basic_wireguard_sites: z.boolean().optional(), disable_basic_wireguard_sites: z.boolean().optional(),
disable_config_managed_domains: 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(), .optional(),
dns: z dns: z

View File

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

View File

@@ -13,16 +13,18 @@
import { build } from "@server/build"; import { build } from "@server/build";
import license from "#private/license/license"; 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") { if (build === "enterprise") {
return await license.isUnlocked(); return await license.isUnlocked();
} }
if (build === "saas") { if (build === "saas") {
const { tier, active } = await getOrgTierData(orgId); return isSubscribed(orgId, tiers);
return (tier == "tier1" || tier == "tier2" || tier == "tier3") && active;
} }
return false; return false;

View File

@@ -14,10 +14,14 @@
import { build } from "@server/build"; import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing"; 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") { if (build === "saas") {
const { tier, active } = await getOrgTierData(orgId); 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; return false;

View File

@@ -17,44 +17,51 @@ import HttpCode from "@server/types/HttpCode";
import { build } from "@server/build"; import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing"; import { getOrgTierData } from "#private/lib/billing";
export async function verifyValidSubscription( export function verifyValidSubscription(tiers: string[]) {
req: Request, return async function (
res: Response, req: Request,
next: NextFunction res: Response,
) { next: NextFunction
try { ): Promise<any> {
if (build != "saas") { 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(); return next();
} } catch (e) {
const orgId = req.params.orgId || req.body.orgId || req.query.orgId || req.userOrgId;
if (!orgId) {
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.INTERNAL_SERVER_ERROR,
"Organization ID is required to verify subscription" "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( authenticated.post(
"/org/:orgId/idp/:idpId/oidc", "/org/:orgId/idp/:idpId/oidc",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription, verifyValidSubscription(),
verifyOrgAccess, verifyOrgAccess,
verifyIdpAccess, verifyIdpAccess,
verifyUserHasAction(ActionsEnum.updateIdp), verifyUserHasAction(ActionsEnum.updateIdp),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,11 @@ export function pullEnv(): Env {
? true ? true
: false, : false,
useOrgOnlyIdp: 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: { branding: {

View File

@@ -35,6 +35,7 @@ export type Env = {
usePangolinDns: boolean; usePangolinDns: boolean;
disableProductHelpBanners: boolean; disableProductHelpBanners: boolean;
useOrgOnlyIdp: boolean; useOrgOnlyIdp: boolean;
disableEnterpriseFeatures: boolean;
}; };
branding: { branding: {
appName?: string; 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 = () => { const getTier = () => {
if (subscriptionStatus?.subscriptions) { if (subscriptionStatus?.subscriptions) {
// Iterate through all subscriptions // Iterate through all subscriptions
@@ -65,9 +55,6 @@ export function SubscriptionStatusProvider({
}; };
const isSubscribed = () => { const isSubscribed = () => {
if (build === "enterprise") {
return true;
}
const { tier, active } = getTier(); const { tier, active } = getTier();
return ( return (
(tier == "tier1" || tier == "tier2" || tier == "tier3") && (tier == "tier1" || tier == "tier2" || tier == "tier3") &&
@@ -82,7 +69,6 @@ export function SubscriptionStatusProvider({
value={{ value={{
subscriptionStatus: subscriptionStatusState, subscriptionStatus: subscriptionStatusState,
updateSubscriptionStatus, updateSubscriptionStatus,
isActive,
getTier, getTier,
isSubscribed, isSubscribed,
subscribed subscribed