Compare commits

..

7 Commits

Author SHA1 Message Date
miloschwartz
69c2212ea0 refactor front end hooks 2026-02-09 20:50:44 -08:00
Owen
10be9bcd56 Fix to use the limits file 2026-02-09 20:39:26 -08:00
Owen
f531def0d2 Comment out stripe usage reporting 2026-02-09 20:30:44 -08:00
miloschwartz
ed40eae655 fix some errors 2026-02-09 20:23:55 -08:00
Owen
ba5ae6ed04 Fix errors 2026-02-09 20:17:14 -08:00
Owen
0a6301697e Handle auto provisioning 2026-02-09 20:11:24 -08:00
Owen
13b4fc6725 Add more tier matrix checks 2026-02-09 19:52:44 -08:00
47 changed files with 294 additions and 331 deletions

View File

@@ -2,7 +2,7 @@ import { Tier } from "@server/types/Tiers";
export enum TierFeature {
OrgOidc = "orgOidc",
CustomAuthenticationDomain = "customAuthenticationDomain",
LoginPageDomain = "loginPageDomain",
DeviceApprovals = "deviceApprovals",
LoginPageBranding = "loginPageBranding",
LogExport = "logExport",
@@ -13,17 +13,13 @@ export enum TierFeature {
DevicePosture = "devicePosture",
TwoFactorEnforcement = "twoFactorEnforcement",
SessionDurationPolicies = "sessionDurationPolicies",
PasswordExpirationPolicies = "passwordExpirationPolicies"
PasswordExpirationPolicies = "passwordExpirationPolicies",
AutoProvisioning = "autoProvisioning"
}
export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.CustomAuthenticationDomain]: [
"tier1",
"tier2",
"tier3",
"enterprise"
],
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
[TierFeature.LoginPageBranding]: ["tier1", "tier3", "enterprise"],
[TierFeature.LogExport]: ["tier3", "enterprise"],
@@ -32,7 +28,23 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.MaintencePage]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"],
[TierFeature.TwoFactorEnforcement]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.SessionDurationPolicies]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.PasswordExpirationPolicies]: ["tier1", "tier2", "tier3", "enterprise"]
[TierFeature.TwoFactorEnforcement]: [
"tier1",
"tier2",
"tier3",
"enterprise"
],
[TierFeature.SessionDurationPolicies]: [
"tier1",
"tier2",
"tier3",
"enterprise"
],
[TierFeature.PasswordExpirationPolicies]: [
"tier1",
"tier2",
"tier3",
"enterprise"
],
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"]
};

View File

@@ -18,7 +18,6 @@ import { sendToClient } from "#dynamic/routers/ws";
import { build } from "@server/build";
import { s3Client } from "@server/lib/s3";
import cache from "@server/lib/cache";
import privateConfig from "@server/private/lib/config";
interface StripeEvent {
identifier?: string;
@@ -48,39 +47,31 @@ export class UsageService {
return;
}
this.bucketName = process.env.S3_BUCKET || undefined;
// this.bucketName = process.env.S3_BUCKET || undefined;
if (
// Only set up event uploading if usage reporting is enabled and bucket name is configured
privateConfig.getRawPrivateConfig().flags.usage_reporting &&
this.bucketName
) {
// Periodically check and upload events
setInterval(() => {
this.checkAndUploadEvents().catch((err) => {
logger.error("Error in periodic event upload:", err);
});
}, 30000); // every 30 seconds
// // Periodically check and upload events
// setInterval(() => {
// this.checkAndUploadEvents().catch((err) => {
// logger.error("Error in periodic event upload:", err);
// });
// }, 30000); // every 30 seconds
// Handle graceful shutdown on SIGTERM
process.on("SIGTERM", async () => {
logger.info(
"SIGTERM received, uploading events before shutdown..."
);
await this.forceUpload();
logger.info("Events uploaded, proceeding with shutdown");
});
// // Handle graceful shutdown on SIGTERM
// process.on("SIGTERM", async () => {
// logger.info(
// "SIGTERM received, uploading events before shutdown..."
// );
// await this.forceUpload();
// logger.info("Events uploaded, proceeding with shutdown");
// });
// Handle SIGINT as well (Ctrl+C)
process.on("SIGINT", async () => {
logger.info(
"SIGINT received, uploading events before shutdown..."
);
await this.forceUpload();
logger.info("Events uploaded, proceeding with shutdown");
process.exit(0);
});
}
// // Handle SIGINT as well (Ctrl+C)
// process.on("SIGINT", async () => {
// logger.info("SIGINT received, uploading events before shutdown...");
// await this.forceUpload();
// logger.info("Events uploaded, proceeding with shutdown");
// process.exit(0);
// });
}
/**
@@ -139,9 +130,9 @@ export class UsageService {
}
// Log event for Stripe
if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
await this.logStripeEvent(featureId, value, customerId);
}
// if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
// await this.logStripeEvent(featureId, value, customerId);
// }
return usage || null;
} catch (error: any) {
@@ -282,9 +273,9 @@ export class UsageService {
}
});
if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
await this.logStripeEvent(featureId, value || 0, customerId);
}
// if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
// await this.logStripeEvent(featureId, value || 0, customerId);
// }
} catch (error) {
logger.error(
`Failed to update count usage for ${orgId}/${featureId}:`,

View File

@@ -33,7 +33,6 @@ import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
import { t } from "@faker-js/faker/dist/airline-DF6RqYmq";
export type ProxyResourcesResults = {
proxyResource: Resource;

View File

@@ -96,7 +96,6 @@ export const privateConfigSchema = z.object({
enable_redis: z.boolean().optional().default(false),
use_pangolin_dns: z.boolean().optional().default(false),
use_org_only_idp: z.boolean().optional().default(false),
usage_reporting: z.boolean().optional().default(false)
})
.optional()
.prefault({}),

View File

@@ -45,7 +45,7 @@ export function verifyValidSubscription(tiers: Tier[]) {
}
const { tier, active } = await getOrgTierData(orgId);
const isTier = tiers.includes(tier || "");
const isTier = tiers.includes(tier as Tier);
if (!active) {
return next(
createHttpError(

View File

@@ -90,7 +90,7 @@ export async function handleSubscriptionUpdated(
const itemsToUpsert = fullSubscription.items.data.map((item) => {
// Try to get featureId from price
let featureId: string | null = getFeatureIdByPriceId(item.price.id) || null;
// If no match, try to preserve existing featureId
if (!featureId) {
const existingItem = existingItems.find(

View File

@@ -280,7 +280,7 @@ authenticated.delete(
authenticated.put(
"/org/:orgId/login-page",
verifyValidLicense,
verifyValidSubscription(tierMatrix.customAuthenticationDomain),
verifyValidSubscription(tierMatrix.loginPageDomain),
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createLoginPage),
logActionAudit(ActionsEnum.createLoginPage),
@@ -290,7 +290,7 @@ authenticated.put(
authenticated.post(
"/org/:orgId/login-page/:loginPageId",
verifyValidLicense,
verifyValidSubscription(tierMatrix.customAuthenticationDomain),
verifyValidSubscription(tierMatrix.loginPageDomain),
verifyOrgAccess,
verifyLoginPageAccess,
verifyUserHasAction(ActionsEnum.updateLoginPage),

View File

@@ -26,14 +26,12 @@ import {
verifyValidLicense
} from "#private/middlewares";
import { ActionsEnum } from "@server/auth/actions";
import {
unauthenticated as ua,
authenticated as a
} from "@server/routers/integration";
import { logActionAudit } from "#private/middlewares";
import config from "#private/lib/config";
import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export const unauthenticated = ua;
export const authenticated = a;
@@ -57,7 +55,7 @@ authenticated.delete(
authenticated.get(
"/org/:orgId/logs/action",
verifyValidLicense,
verifyValidSubscription,
verifyValidSubscription(tierMatrix.actionLogs),
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs),
logs.queryActionAuditLogs
@@ -66,7 +64,7 @@ authenticated.get(
authenticated.get(
"/org/:orgId/logs/action/export",
verifyValidLicense,
verifyValidSubscription,
verifyValidSubscription(tierMatrix.logExport),
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs),
@@ -76,7 +74,7 @@ authenticated.get(
authenticated.get(
"/org/:orgId/logs/access",
verifyValidLicense,
verifyValidSubscription,
verifyValidSubscription(tierMatrix.accessLogs),
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs),
logs.queryAccessAuditLogs
@@ -85,7 +83,7 @@ authenticated.get(
authenticated.get(
"/org/:orgId/logs/access/export",
verifyValidLicense,
verifyValidSubscription,
verifyValidSubscription(tierMatrix.logExport),
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs),
@@ -95,6 +93,7 @@ authenticated.get(
authenticated.put(
"/org/:orgId/idp/oidc",
verifyValidLicense,
verifyValidSubscription(tierMatrix.orgOidc),
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.createIdp),
logActionAudit(ActionsEnum.createIdp),
@@ -104,6 +103,7 @@ authenticated.put(
authenticated.post(
"/org/:orgId/idp/:idpId/oidc",
verifyValidLicense,
verifyValidSubscription(tierMatrix.orgOidc),
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess,
verifyApiKeyHasAction(ActionsEnum.updateIdp),

View File

@@ -25,6 +25,8 @@ import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
@@ -100,12 +102,21 @@ export async function createOrgOidcIdp(
emailPath,
namePath,
name,
autoProvision,
variant,
roleMapping,
tags
} = parsedBody.data;
let { autoProvision } = parsedBody.data;
const subscribed = await isSubscribed(
orgId,
tierMatrix.deviceApprovals
);
if (!subscribed) {
autoProvision = false;
}
const key = config.getRawConfig().server.secret!;
const encryptedSecret = encrypt(clientSecret, key);

View File

@@ -24,6 +24,8 @@ import { idp, idpOidcConfig } from "@server/db";
import { eq, and } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z
.object({
@@ -106,11 +108,20 @@ export async function updateOrgOidcIdp(
emailPath,
namePath,
name,
autoProvision,
roleMapping,
tags
} = parsedBody.data;
let { autoProvision } = parsedBody.data;
const subscribed = await isSubscribed(
orgId,
tierMatrix.deviceApprovals
);
if (!subscribed) {
autoProvision = false;
}
// Check if IDP exists and is of type OIDC
const [existingIdp] = await db
.select()

View File

@@ -17,7 +17,7 @@ import {
ResourceHeaderAuthExtendedCompatibility,
ResourcePassword,
ResourcePincode,
ResourceRule,
ResourceRule
} from "@server/db";
import config from "@server/lib/config";
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
@@ -40,6 +40,7 @@ import { logRequestAudit } from "./logRequestAudit";
import cache from "@server/lib/cache";
import { APP_VERSION } from "@server/lib/consts";
import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const verifyResourceSessionSchema = z.object({
sessions: z.record(z.string(), z.string()).optional(),
@@ -796,7 +797,10 @@ async function notAllowed(
) {
let loginPage: LoginPage | null = null;
if (orgId) {
const subscribed = await isSubscribed(orgId);
const subscribed = await isSubscribed(
orgId,
tierMatrix.loginPageDomain
);
if (subscribed) {
loginPage = await getOrgLoginPage(orgId);
}
@@ -850,7 +854,7 @@ async function headerAuthChallenged(
) {
let loginPage: LoginPage | null = null;
if (orgId) {
const subscribed = await isSubscribed(orgId);
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain);
if (subscribed) {
loginPage = await getOrgLoginPage(orgId);
}
@@ -1037,7 +1041,11 @@ export function isPathAllowed(pattern: string, path: string): boolean {
const MAX_RECURSION_DEPTH = 100;
// Recursive function to try different wildcard matches
function matchSegments(patternIndex: number, pathIndex: number, depth: number = 0): boolean {
function matchSegments(
patternIndex: number,
pathIndex: number,
depth: number = 0
): boolean {
// Check recursion depth limit
if (depth > MAX_RECURSION_DEPTH) {
logger.warn(
@@ -1123,7 +1131,11 @@ export function isPathAllowed(pattern: string, path: string): boolean {
logger.debug(
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
);
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
return matchSegments(
patternIndex + 1,
pathIndex + 1,
depth + 1
);
}
logger.debug(

View File

@@ -15,6 +15,7 @@ import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto";
import { build } from "@server/build";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z
.object({
@@ -112,7 +113,10 @@ export async function generateOidcUrl(
}
if (build === "saas") {
const subscribed = await isSubscribed(orgId);
const subscribed = await isSubscribed(
orgId,
tierMatrix.orgOidc
);
if (!subscribed) {
return next(
createHttpError(

View File

@@ -34,6 +34,8 @@ import { FeatureId } from "@server/lib/billing";
import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const ensureTrailingSlash = (url: string): string => {
return url;
@@ -326,6 +328,33 @@ export async function validateOidcCallback(
.where(eq(idpOrg.idpId, existingIdp.idp.idpId))
.innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId));
allOrgs = idpOrgs.map((o) => o.orgs);
// TODO: when there are multiple orgs we need to do this better!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1
if (allOrgs.length > 1) {
// for some reason there is more than one org
logger.error(
"More than one organization linked to this IdP. This should not happen with auto-provisioning enabled."
);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Multiple organizations linked to this IdP. Please contact support."
)
);
}
const subscribed = await isSubscribed(
allOrgs[0].orgId,
tierMatrix.autoProvisioning
);
if (subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
} else {
allOrgs = await db.select().from(orgs);
}

View File

@@ -18,7 +18,7 @@ import config from "@server/lib/config";
import { APP_VERSION } from "@server/lib/consts";
export const newtGetTokenBodySchema = z.object({
// newtId: z.string(),
newtId: z.string(),
secret: z.string(),
token: z.string().optional()
});

View File

@@ -24,6 +24,7 @@ import { createCertificate } from "#dynamic/routers/certificates/createCertifica
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const updateResourceParamsSchema = z.strictObject({
resourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -341,7 +342,7 @@ async function updateHttpResource(
headers = null;
}
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
const isLicensed = await isLicensedOrSubscribed(resource.orgId, tierMatrix.maintencePage);
if (!isLicensed) {
updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined;

View File

@@ -12,6 +12,7 @@ import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const createRoleParamsSchema = z.strictObject({
orgId: z.string()
@@ -100,7 +101,7 @@ export async function createRole(
);
}
const isLicensed = await isLicensedOrSubscribed(orgId);
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
if (!isLicensed) {
roleData.requireDeviceApproval = undefined;
}

View File

@@ -10,6 +10,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { OpenAPITags, registry } from "@server/openApi";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const updateRoleParamsSchema = z.strictObject({
roleId: z.string().transform(Number).pipe(z.int().positive())
@@ -110,7 +111,7 @@ export async function updateRole(
);
}
const isLicensed = await isLicensedOrSubscribed(orgId);
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
if (!isLicensed) {
updateData.requireDeviceApproval = undefined;
}

View File

@@ -15,6 +15,7 @@ import { FeatureId } from "@server/lib/billing";
import { build } from "@server/build";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
@@ -127,7 +128,10 @@ export async function createOrgUser(
);
} else if (type === "oidc") {
if (build === "saas") {
const subscribed = await isSubscribed(orgId);
const subscribed = await isSubscribed(
orgId,
tierMatrix.orgOidc
);
if (subscribed) {
return next(
createHttpError(

View File

@@ -34,15 +34,7 @@ import {
CredenzaTitle
} from "@app/components/Credenza";
import { cn } from "@app/lib/cn";
import {
CreditCard,
ExternalLink,
Users,
Globe,
Server,
Layout,
Check
} from "lucide-react";
import { CreditCard, ExternalLink, Check } from "lucide-react";
import {
GetOrgSubscriptionResponse,
GetOrgUsageResponse
@@ -50,73 +42,45 @@ import {
import { useTranslations } from "use-intl";
import Link from "next/link";
import { Tier } from "@server/types/Tiers";
import { w } from "@faker-js/faker/dist/airline-DF6RqYmq";
import { tier1LimitSet, tier2LimitSet, tier3LimitSet } from "@server/lib/billing/limitSet";
import { FeatureId } from "@server/lib/billing/features";
// Plan tier definitions matching the mockup
type PlanId = "free" | "homelab" | "team" | "business" | "enterprise";
type PlanId = "starter" | "home" | "team" | "business" | "enterprise";
interface PlanOption {
type PlanOption = {
id: PlanId;
name: string;
price: string;
priceDetail?: string;
tierType: Tier | null;
}
// Tier limits for display in confirmation dialog
interface TierLimits {
sites: number;
users: number;
domains: number;
remoteNodes: number;
}
const tierLimits: Record<Tier, TierLimits> = {
tier1: {
sites: 3,
users: 3,
domains: 3,
remoteNodes: 1
},
tier2: {
sites: 10,
users: 150,
domains: 250,
remoteNodes: 5
},
tier3: {
sites: 10,
users: 150,
domains: 250,
remoteNodes: 5
}
};
const planOptions: PlanOption[] = [
{
id: "free",
name: "Free",
price: "Free",
id: "starter",
name: "Starter",
price: "Starter",
tierType: null
},
{
id: "homelab",
name: "Homelab",
price: "$15",
id: "home",
name: "Home",
price: "$12.50",
priceDetail: "/ month",
tierType: "tier1"
},
{
id: "team",
name: "Team",
price: "$5",
price: "$4",
priceDetail: "per user / month",
tierType: "tier2"
},
{
id: "business",
name: "Business",
price: "$10",
price: "$9",
priceDetail: "per user / month",
tierType: "tier3"
},
@@ -128,6 +92,34 @@ const planOptions: PlanOption[] = [
}
];
// Tier limits mapping derived from limit sets
const tierLimits: Record<Tier, { users: number; sites: number; domains: number; remoteNodes: number }> = {
tier1: {
users: tier1LimitSet[FeatureId.USERS]?.value ?? 0,
sites: tier1LimitSet[FeatureId.SITES]?.value ?? 0,
domains: tier1LimitSet[FeatureId.DOMAINS]?.value ?? 0,
remoteNodes: tier1LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0
},
tier2: {
users: tier2LimitSet[FeatureId.USERS]?.value ?? 0,
sites: tier2LimitSet[FeatureId.SITES]?.value ?? 0,
domains: tier2LimitSet[FeatureId.DOMAINS]?.value ?? 0,
remoteNodes: tier2LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0
},
tier3: {
users: tier3LimitSet[FeatureId.USERS]?.value ?? 0,
sites: tier3LimitSet[FeatureId.SITES]?.value ?? 0,
domains: tier3LimitSet[FeatureId.DOMAINS]?.value ?? 0,
remoteNodes: tier3LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0
},
enterprise: {
users: 0, // Custom for enterprise
sites: 0, // Custom for enterprise
domains: 0, // Custom for enterprise
remoteNodes: 0 // Custom for enterprise
}
};
export default function BillingPage() {
const { org } = useOrgContext();
const envContext = useEnvContext();
@@ -156,9 +148,7 @@ export default function BillingPage() {
const [hasSubscription, setHasSubscription] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [currentTier, setCurrentTier] = useState<
Tier | null
>(null);
const [currentTier, setCurrentTier] = useState<Tier | null>(null);
// Usage IDs
const SITES = "sites";
@@ -169,7 +159,7 @@ export default function BillingPage() {
// Confirmation dialog state
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [pendingTier, setPendingTier] = useState<{
tier: Tier,
tier: Tier;
action: "upgrade" | "downgrade";
planName: string;
price: string;
@@ -195,9 +185,7 @@ export default function BillingPage() {
setTierSubscription(tierSub || null);
if (tierSub?.subscription) {
setCurrentTier(
tierSub.subscription.type as Tier
);
setCurrentTier(tierSub.subscription.type as Tier);
setHasSubscription(
tierSub.subscription.status === "active"
);
@@ -241,9 +229,7 @@ export default function BillingPage() {
fetchUsage();
}, [org.org.orgId]);
const handleStartSubscription = async (
tier: Tier
) => {
const handleStartSubscription = async (tier: Tier) => {
setIsLoading(true);
try {
const response = await api.post<AxiosResponse<string>>(
@@ -357,9 +343,9 @@ export default function BillingPage() {
// Get current plan ID from tier
const getCurrentPlanId = (): PlanId => {
if (!hasSubscription || !currentTier) return "free";
if (!hasSubscription || !currentTier) return "starter";
const plan = planOptions.find((p) => p.tierType === currentTier);
return plan?.id || "free";
return plan?.id || "starter";
};
const currentPlanId = getCurrentPlanId();
@@ -376,8 +362,8 @@ export default function BillingPage() {
}
if (plan.id === currentPlanId) {
// If it's the free plan (free with no subscription), show as current but disabled
if (plan.id === "free" && !hasSubscription) {
// If it's the starter plan (starter with no subscription), show as current but disabled
if (plan.id === "starter" && !hasSubscription) {
return {
label: "Current Plan",
action: () => {},
@@ -452,7 +438,10 @@ export default function BillingPage() {
// Calculate current usage cost for display
const getUserCount = () => getUsageValue(USERS);
const getPricePerUser = () => {
console.log("Calculating price per user, tierSubscription:", tierSubscription);
console.log(
"Calculating price per user, tierSubscription:",
tierSubscription
);
if (!tierSubscription?.items) return 0;
// Find the subscription item for USERS feature

View File

@@ -46,7 +46,7 @@ import IdpTypeBadge from "@app/components/IdpTypeBadge";
import { useTranslations } from "next-intl";
import { AxiosResponse } from "axios";
import { ListRolesResponse } from "@server/routers/role";
import AutoProvisionConfigWidget from "@app/components/private/AutoProvisionConfigWidget";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
export default function GeneralPage() {
const { env } = useEnvContext();

View File

@@ -1,6 +1,6 @@
"use client";
import AutoProvisionConfigWidget from "@app/components/private/AutoProvisionConfigWidget";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import {
SettingsContainer,
SettingsSection,

View File

@@ -2,7 +2,7 @@ import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { AxiosResponse } from "axios";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import IdpTable, { IdpRow } from "@app/components/private/OrgIdpTable";
import IdpTable, { IdpRow } from "@app/components/OrgIdpTable";
import { getTranslations } from "next-intl/server";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";

View File

@@ -23,9 +23,6 @@ import {
} from "@server/routers/remoteExitNode/types";
import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { build } from "@server/build";
import {
InfoSection,
InfoSectionContent,
@@ -36,6 +33,8 @@ import CopyToClipboard from "@app/components/CopyToClipboard";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon } from "lucide-react";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export default function CredentialsPage() {
const { env } = useEnvContext();
@@ -45,6 +44,8 @@ export default function CredentialsPage() {
const t = useTranslations();
const { remoteExitNode } = useRemoteExitNodeContext();
const { isPaidUser } = usePaidStatus();
const [modalOpen, setModalOpen] = useState(false);
const [credentials, setCredentials] =
useState<PickRemoteExitNodeDefaultsResponse | null>(null);
@@ -57,16 +58,6 @@ export default function CredentialsPage() {
const [showCredentialsAlert, setShowCredentialsAlert] = useState(false);
const [shouldDisconnect, setShouldDisconnect] = useState(true);
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext();
const isSecurityFeatureDisabled = () => {
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return isEnterpriseNotLicensed || isSaasNotSubscribed;
};
const handleConfirmRegenerate = async () => {
try {
const response = await api.get<
@@ -203,7 +194,7 @@ export default function CredentialsPage() {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.rotateCredentials)}
>
{t("regenerateCredentialsButton")}
</Button>
@@ -212,7 +203,7 @@ export default function CredentialsPage() {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.rotateCredentials)}
>
{t("remoteExitNodeRegenerateAndDisconnect")}
</Button>

View File

@@ -47,7 +47,8 @@ import { ListIdpsResponse } from "@server/routers/idp";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
import Image from "next/image";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
type UserType = "internal" | "oidc";
@@ -75,7 +76,7 @@ export default function Page() {
const api = createApiClient({ env });
const t = useTranslations();
const subscription = useSubscriptionStatusContext();
const { hasSaasSubscription } = usePaidStatus();
const [selectedOption, setSelectedOption] = useState<string | null>(
"internal"
@@ -237,7 +238,7 @@ export default function Page() {
}
async function fetchIdps() {
if (build === "saas" && !subscription?.subscribed) {
if (build === "saas" && !hasSaasSubscription(tierMatrix.orgOidc)) {
return;
}

View File

@@ -19,9 +19,6 @@ import { useTranslations } from "next-intl";
import { PickClientDefaultsResponse } from "@server/routers/client";
import { useClientContext } from "@app/hooks/useClientContext";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { build } from "@server/build";
import {
InfoSection,
InfoSectionContent,
@@ -33,6 +30,8 @@ import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon } from "lucide-react";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { OlmInstallCommands } from "@app/components/olm-install-commands";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export default function CredentialsPage() {
const { env } = useEnvContext();
@@ -54,17 +53,7 @@ export default function CredentialsPage() {
const [showCredentialsAlert, setShowCredentialsAlert] = useState(false);
const [shouldDisconnect, setShouldDisconnect] = useState(true);
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext();
const isSecurityFeatureDisabled = () => {
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return (
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
);
};
const { isPaidUser } = usePaidStatus();
const handleConfirmRegenerate = async () => {
try {
@@ -191,7 +180,7 @@ export default function CredentialsPage() {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.rotateCredentials)}
>
{t("regenerateCredentialsButton")}
</Button>
@@ -200,7 +189,7 @@ export default function CredentialsPage() {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.rotateCredentials)}
>
{t("clientRegenerateAndDisconnect")}
</Button>

View File

@@ -616,7 +616,7 @@ export default function GeneralPage() {
{t("diskEncrypted")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.diskEncrypted
@@ -634,7 +634,7 @@ export default function GeneralPage() {
{t("firewallEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.firewallEnabled
@@ -653,7 +653,7 @@ export default function GeneralPage() {
{t("autoUpdatesEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.autoUpdatesEnabled
@@ -671,7 +671,7 @@ export default function GeneralPage() {
{t("tpmAvailable")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.tpmAvailable
@@ -693,7 +693,7 @@ export default function GeneralPage() {
)}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.windowsAntivirusEnabled
@@ -711,7 +711,7 @@ export default function GeneralPage() {
{t("macosSipEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.macosSipEnabled
@@ -733,7 +733,7 @@ export default function GeneralPage() {
)}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.macosGatekeeperEnabled
@@ -755,7 +755,7 @@ export default function GeneralPage() {
)}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.macosFirewallStealthMode
@@ -774,7 +774,7 @@ export default function GeneralPage() {
{t("linuxAppArmorEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.linuxAppArmorEnabled
@@ -793,7 +793,7 @@ export default function GeneralPage() {
{t("linuxSELinuxEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
{isPaidUser(tierMatrix.devicePosture)
? formatPostureValue(
client.posture
.linuxSELinuxEnabled

View File

@@ -1,5 +1,5 @@
import AuthPageBrandingForm from "@app/components/AuthPageBrandingForm";
import AuthPageSettings from "@app/components/private/AuthPageSettings";
import AuthPageSettings from "@app/components/AuthPageSettings";
import { SettingsContainer } from "@app/components/Settings";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";

View File

@@ -13,14 +13,13 @@ import { ArrowUpRight, Key, User } from "lucide-react";
import Link from "next/link";
import { ColumnFilter } from "@app/components/ColumnFilter";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { build } from "@server/build";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
import axios from "axios";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export default function GeneralPage() {
const router = useRouter();
@@ -28,8 +27,8 @@ export default function GeneralPage() {
const api = createApiClient(useEnvContext());
const t = useTranslations();
const { orgId } = useParams();
const subscription = useSubscriptionStatusContext();
const { isUnlocked } = useLicenseStatusContext();
const { isPaidUser } = usePaidStatus();
const [rows, setRows] = useState<any[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
@@ -208,11 +207,7 @@ export default function GeneralPage() {
}
) => {
console.log("Date range changed:", { startDate, endDate, page, size });
if (
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked()) ||
build === "oss"
) {
if (!isPaidUser(tierMatrix.accessLogs) || build === "oss") {
console.log(
"Access denied: subscription inactive or license locked"
);
@@ -642,11 +637,7 @@ export default function GeneralPage() {
// Row expansion props
expandable={true}
renderExpandedRow={renderExpandedRow}
disabled={
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked()) ||
build === "oss"
}
disabled={!isPaidUser(tierMatrix.accessLogs) || build === "oss"}
/>
</>
);

View File

@@ -4,15 +4,14 @@ import { DateTimeValue } from "@app/components/DateTimePicker";
import { LogDataTable } from "@app/components/LogDataTable";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { ColumnDef } from "@tanstack/react-table";
import axios from "axios";
import { Key, User } from "lucide-react";
@@ -26,8 +25,8 @@ export default function GeneralPage() {
const t = useTranslations();
const { orgId } = useParams();
const searchParams = useSearchParams();
const subscription = useSubscriptionStatusContext();
const { isUnlocked } = useLicenseStatusContext();
const { isPaidUser } = usePaidStatus();
const [rows, setRows] = useState<any[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
@@ -195,10 +194,7 @@ export default function GeneralPage() {
}
) => {
console.log("Date range changed:", { startDate, endDate, page, size });
if (
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked())
) {
if (!isPaidUser(tierMatrix.actionLogs)) {
console.log(
"Access denied: subscription inactive or license locked"
);
@@ -496,11 +492,7 @@ export default function GeneralPage() {
// Row expansion props
expandable={true}
renderExpandedRow={renderExpandedRow}
disabled={
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked()) ||
build === "oss"
}
disabled={!isPaidUser(tierMatrix.actionLogs) || build === "oss"}
/>
</>
);

View File

@@ -1,54 +1,3 @@
"use client";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import AuthPageSettings, {
AuthPageSettingsRef
} from "@app/components/private/AuthPageSettings";
import { Button } from "@app/components/ui/button";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
import { toast } from "@app/hooks/useToast";
import { useState, useRef } from "react";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { formatAxiosError } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { DeleteOrgResponse, ListUserOrgsResponse } from "@server/routers/org";
import { useRouter } from "next/navigation";
import {
SettingsContainer,
SettingsSection,
SettingsSectionHeader,
SettingsSectionTitle,
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionForm,
SettingsSectionFooter
} from "@app/components/Settings";
import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const router = useRouter();
const api = createApiClient(useEnvContext());
const t = useTranslations();
const { env } = useEnvContext();
return <p>dfas</p>;
return null;
}

View File

@@ -50,9 +50,6 @@ import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import { build } from "@server/build";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import {
@@ -64,6 +61,7 @@ import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { GetResourceResponse } from "@server/routers/resource/getResource";
import type { ResourceContextType } from "@app/contexts/resourceContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
type MaintenanceSectionFormProps = {
resource: GetResourceResponse;
@@ -77,8 +75,6 @@ function MaintenanceSectionForm({
const { env } = useEnvContext();
const t = useTranslations();
const api = createApiClient({ env });
const { isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext();
const { isPaidUser } = usePaidStatus();
const MaintenanceFormSchema = z.object({
@@ -159,15 +155,6 @@ function MaintenanceSectionForm({
}
}
const isSecurityFeatureDisabled = () => {
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return (
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
);
};
if (!resource.http) {
return null;
}
@@ -197,7 +184,7 @@ function MaintenanceSectionForm({
name="maintenanceModeEnabled"
render={({ field }) => {
const isDisabled =
isSecurityFeatureDisabled() ||
isPaidUser(tierMatrix.maintencePage) ||
resource.http === false;
return (
@@ -264,7 +251,7 @@ function MaintenanceSectionForm({
defaultValue={
field.value
}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.maintencePage)}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-start space-x-3 space-y-0">
@@ -337,7 +324,7 @@ function MaintenanceSectionForm({
<FormControl>
<Input
{...field}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.maintencePage)}
placeholder="We'll be back soon!"
/>
</FormControl>
@@ -363,7 +350,7 @@ function MaintenanceSectionForm({
<Textarea
{...field}
rows={4}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.maintencePage)}
placeholder={t(
"maintenancePageMessagePlaceholder"
)}
@@ -392,7 +379,7 @@ function MaintenanceSectionForm({
<FormControl>
<Input
{...field}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.maintencePage)}
placeholder={t(
"maintenanceTime"
)}

View File

@@ -20,9 +20,6 @@ import { PickSiteDefaultsResponse } from "@server/routers/site";
import { useSiteContext } from "@app/hooks/useSiteContext";
import { generateKeypair } from "../wireguardConfig";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { build } from "@server/build";
import {
InfoSection,
InfoSectionContent,
@@ -40,6 +37,8 @@ import {
import { QRCodeCanvas } from "qrcode.react";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export default function CredentialsPage() {
const { env } = useEnvContext();
@@ -65,17 +64,7 @@ export default function CredentialsPage() {
const [loadingDefaults, setLoadingDefaults] = useState(false);
const [shouldDisconnect, setShouldDisconnect] = useState(true);
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext();
const isSecurityFeatureDisabled = () => {
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
const isSaasNotSubscribed =
build === "saas" && !subscription?.isSubscribed();
return (
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
);
};
const { isPaidUser } = usePaidStatus();
// Fetch site defaults for wireguard sites to show in obfuscated config
useEffect(() => {
@@ -279,7 +268,7 @@ export default function CredentialsPage() {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.rotateCredentials)}
>
{t("regenerateCredentialsButton")}
</Button>
@@ -288,7 +277,7 @@ export default function CredentialsPage() {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.rotateCredentials)}
>
{t("siteRegenerateAndDisconnect")}
</Button>
@@ -389,7 +378,7 @@ export default function CredentialsPage() {
<SettingsSectionFooter>
<Button
onClick={() => setModalOpen(true)}
disabled={isSecurityFeatureDisabled()}
disabled={isPaidUser(tierMatrix.rotateCredentials)}
>
{t("siteRegenerateAndDisconnect")}
</Button>

View File

@@ -14,7 +14,7 @@ import {
LoadLoginPageResponse
} from "@server/routers/loginPage/types";
import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types";
import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken";
import ValidateSessionTransferToken from "@app/components/ValidateSessionTransferToken";
import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed";
import { OrgSelectionForm } from "@app/components/OrgSelectionForm";
import OrgLoginPage from "@app/components/OrgLoginPage";

View File

@@ -5,7 +5,7 @@ import { ThemeProvider } from "@app/providers/ThemeProvider";
import EnvProvider from "@app/providers/EnvProvider";
import { pullEnv } from "@app/lib/pullEnv";
import ThemeDataProvider from "@app/providers/ThemeDataProvider";
import SplashImage from "@app/components/private/SplashImage";
import SplashImage from "@app/components/SplashImage";
import SupportStatusProvider from "@app/providers/SupporterStatusProvider";
import { priv } from "@app/lib/api";
import { AxiosResponse } from "axios";

View File

@@ -28,7 +28,7 @@ import { ListDomainsResponse } from "@server/routers/domain";
import { DomainRow } from "@app/components/DomainsTable";
import { toUnicode } from "punycode";
import { Globe, Trash2 } from "lucide-react";
import CertificateStatus from "@app/components/private/CertificateStatus";
import CertificateStatus from "@app/components/CertificateStatus";
import {
Credenza,
CredenzaBody,
@@ -42,10 +42,10 @@ import {
import DomainPicker from "@app/components/DomainPicker";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import { InfoPopup } from "@app/components/ui/info-popup";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { build } from "@server/build";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { PaidFeaturesAlert } from "../PaidFeaturesAlert";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
// Auth page form schema
const AuthPageFormSchema = z.object({
@@ -75,7 +75,7 @@ function AuthPageSettings({
const t = useTranslations();
const { env } = useEnvContext();
const { hasSaasSubscription } = usePaidStatus();
const { isPaidUser } = usePaidStatus();
// Auth page domain state
const [loginPage, setLoginPage] = useState(defaultLoginPage);
@@ -177,7 +177,7 @@ function AuthPageSettings({
try {
// Handle auth page domain
if (data.authPageDomainId) {
if (build === "enterprise" || hasSaasSubscription) {
if (isPaidUser(tierMatrix.loginPageDomain)) {
const sanitizedSubdomain = data.authPageSubdomain
? finalizeSubdomainSanitize(data.authPageSubdomain)
: "";
@@ -361,7 +361,11 @@ function AuthPageSettings({
onClick={() =>
setEditDomainOpen(true)
}
disabled={!hasSaasSubscription}
disabled={
!isPaidUser(
tierMatrix.loginPageDomain
)
}
>
{form.watch("authPageDomainId")
? t("changeDomain")
@@ -376,7 +380,9 @@ function AuthPageSettings({
clearAuthPageDomain
}
disabled={
!hasSaasSubscription
!isPaidUser(
tierMatrix.loginPageDomain
)
}
>
<Trash2 size="14" />
@@ -395,7 +401,9 @@ function AuthPageSettings({
{env.flags.usePangolinDns &&
(build === "enterprise" ||
!hasSaasSubscription) &&
!isPaidUser(
tierMatrix.loginPageDomain
)) &&
loginPage?.domainId &&
loginPage?.fullDomain &&
!hasUnsavedChanges && (
@@ -424,7 +432,7 @@ function AuthPageSettings({
disabled={
isSubmitting ||
!hasUnsavedChanges ||
!hasSaasSubscription
!isPaidUser(tierMatrix.loginPageDomain)
}
>
{t("saveAuthPageDomain")}
@@ -477,7 +485,10 @@ function AuthPageSettings({
handleDomainSelection(selectedDomain);
}
}}
disabled={!selectedDomain || !hasSaasSubscription}
disabled={
!selectedDomain ||
!isPaidUser(tierMatrix.loginPageDomain)
}
>
{t("selectDomain")}
</Button>

View File

@@ -2,11 +2,9 @@
import { useState } from "react";
import { Button } from "@app/components/ui/button";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { useTranslations } from "next-intl";
import { Separator } from "./ui/separator";
import LoginPasswordForm from "./LoginPasswordForm";
import IdpLoginButtons from "./private/IdpLoginButtons";
import IdpLoginButtons from "./IdpLoginButtons";
import { LookupUserResponse } from "@server/routers/auth/lookupUser";
import UserProfileCard from "./UserProfileCard";

View File

@@ -2,7 +2,7 @@
import { ColumnDef } from "@tanstack/react-table";
import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { IdpDataTable } from "@app/components/private/OrgIdpDataTable";
import { IdpDataTable } from "@app/components/OrgIdpDataTable";
import { Button } from "@app/components/ui/button";
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
import { useState } from "react";

View File

@@ -3,7 +3,7 @@ import {
LoadLoginPageBrandingResponse,
LoadLoginPageResponse
} from "@server/routers/loginPage/types";
import IdpLoginButtons from "@app/components/private/IdpLoginButtons";
import IdpLoginButtons from "@app/components/IdpLoginButtons";
import {
Card,
CardContent,

View File

@@ -11,7 +11,7 @@ import {
InfoSectionTitle
} from "@app/components/InfoSection";
import { useTranslations } from "next-intl";
import CertificateStatus from "@app/components/private/CertificateStatus";
import CertificateStatus from "@app/components/CertificateStatus";
import { toUnicode } from "punycode";
import { useEnvContext } from "@app/hooks/useEnvContext";

View File

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