mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
Compare commits
13 Commits
300b4a3706
...
a095dddd01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a095dddd01 | ||
|
|
1b5cfaa49b | ||
|
|
66f3fabbae | ||
|
|
0be8fb7931 | ||
|
|
431e6ffaae | ||
|
|
7d8185e0ee | ||
|
|
dff45748bd | ||
|
|
e6464929ff | ||
|
|
122053939d | ||
|
|
410ed3949b | ||
|
|
efc6ef3075 | ||
|
|
313acabc86 | ||
|
|
a8f6b6c1da |
@@ -55,7 +55,7 @@
|
||||
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||
"sitesBannerTitle": "Connect Any Network",
|
||||
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
||||
"sitesBannerButtonText": "Install Site",
|
||||
"sitesBannerButtonText": "Install Site Connector",
|
||||
"approvalsBannerTitle": "Approve or Deny Device Access",
|
||||
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
|
||||
"approvalsBannerButtonText": "Learn More",
|
||||
@@ -79,8 +79,8 @@
|
||||
"siteConfirmCopy": "I have copied the config",
|
||||
"searchSitesProgress": "Search sites...",
|
||||
"siteAdd": "Add Site",
|
||||
"siteInstallNewt": "Install Newt",
|
||||
"siteInstallNewtDescription": "Get Newt running on your system",
|
||||
"siteInstallNewt": "Install Site",
|
||||
"siteInstallNewtDescription": "Install the site connector for your system",
|
||||
"WgConfiguration": "WireGuard Configuration",
|
||||
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
||||
"operatingSystem": "Operating System",
|
||||
@@ -1565,8 +1565,8 @@
|
||||
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
||||
"selectSites": "Select sites",
|
||||
"sitesDescription": "The client will have connectivity to the selected sites",
|
||||
"clientInstallOlm": "Install Olm",
|
||||
"clientInstallOlmDescription": "Get Olm running on your system",
|
||||
"clientInstallOlm": "Install Machine Client",
|
||||
"clientInstallOlmDescription": "Install the machine client for your system",
|
||||
"clientOlmCredentials": "Credentials",
|
||||
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
|
||||
"olmEndpoint": "Endpoint",
|
||||
@@ -2267,6 +2267,7 @@
|
||||
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
|
||||
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature.",
|
||||
"certResolver": "Certificate Resolver",
|
||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||
"selectCertResolver": "Select Certificate Resolver",
|
||||
|
||||
@@ -97,6 +97,7 @@ export const subscriptionItems = pgTable("subscriptionItems", {
|
||||
}),
|
||||
planId: varchar("planId", { length: 255 }).notNull(),
|
||||
priceId: varchar("priceId", { length: 255 }),
|
||||
featureId: varchar("featureId", { length: 255 }),
|
||||
meterId: varchar("meterId", { length: 255 }),
|
||||
unitAmount: real("unitAmount"),
|
||||
tiers: text("tiers"),
|
||||
|
||||
@@ -86,6 +86,7 @@ export const subscriptionItems = sqliteTable("subscriptionItems", {
|
||||
}),
|
||||
planId: text("planId").notNull(),
|
||||
priceId: text("priceId"),
|
||||
featureId: text("featureId"),
|
||||
meterId: text("meterId"),
|
||||
unitAmount: real("unitAmount"),
|
||||
tiers: text("tiers"),
|
||||
|
||||
@@ -116,6 +116,26 @@ export function getScaleFeaturePriceSet(): FeaturePriceSet {
|
||||
}
|
||||
}
|
||||
|
||||
export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
|
||||
// Check all feature price sets
|
||||
const allPriceSets = [
|
||||
getHomeLabFeaturePriceSet(),
|
||||
getStarterFeaturePriceSet(),
|
||||
getScaleFeaturePriceSet()
|
||||
];
|
||||
|
||||
for (const priceSet of allPriceSets) {
|
||||
const entry = (Object.entries(priceSet) as [FeatureId, string][]).find(
|
||||
([_, price]) => price === priceId
|
||||
);
|
||||
if (entry) {
|
||||
return entry[0];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getLineItems(
|
||||
featurePriceSet: FeaturePriceSet,
|
||||
orgId: string,
|
||||
|
||||
@@ -8,77 +8,60 @@ export type LimitSet = Partial<{
|
||||
}>;
|
||||
|
||||
export const sandboxLimitSet: LimitSet = {
|
||||
[FeatureId.SITES]: { value: 1, description: "Sandbox limit" }, // 1 site up for 2 days
|
||||
[FeatureId.USERS]: { value: 1, description: "Sandbox limit" },
|
||||
[FeatureId.EGRESS_DATA_MB]: { value: 1000, description: "Sandbox limit" }, // 1 GB
|
||||
[FeatureId.SITES]: { value: 1, description: "Sandbox limit" },
|
||||
[FeatureId.DOMAINS]: { value: 0, description: "Sandbox limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Sandbox limit" }
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Sandbox limit" },
|
||||
};
|
||||
|
||||
export const freeLimitSet: LimitSet = {
|
||||
[FeatureId.SITES]: { value: 3, description: "Free tier limit" }, // 1 site up for 32 days
|
||||
[FeatureId.USERS]: { value: 3, description: "Free tier limit" },
|
||||
[FeatureId.EGRESS_DATA_MB]: {
|
||||
value: 25000,
|
||||
description: "Free tier limit"
|
||||
}, // 25 GB
|
||||
[FeatureId.DOMAINS]: { value: 3, description: "Free tier limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Free tier limit" }
|
||||
[FeatureId.USERS]: { value: 5, description: "Starter limit" },
|
||||
[FeatureId.SITES]: { value: 5, description: "Starter limit" },
|
||||
[FeatureId.DOMAINS]: { value: 5, description: "Starter limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Starter limit" },
|
||||
};
|
||||
|
||||
export const homeLabLimitSet: LimitSet = {
|
||||
[FeatureId.SITES]: { value: 3, description: "Home lab limit" }, // 1 site up for 32 days
|
||||
[FeatureId.USERS]: { value: 3, description: "Home lab limit" },
|
||||
[FeatureId.EGRESS_DATA_MB]: {
|
||||
value: 25000,
|
||||
description: "Home lab limit"
|
||||
}, // 25 GB
|
||||
[FeatureId.DOMAINS]: { value: 3, description: "Home lab limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Home lab limit" }
|
||||
export const tier1LimitSet: LimitSet = {
|
||||
[FeatureId.USERS]: { value: 7, description: "Home limit" },
|
||||
[FeatureId.SITES]: { value: 10, description: "Home limit" },
|
||||
[FeatureId.DOMAINS]: { value: 10, description: "Home limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Home limit" },
|
||||
};
|
||||
|
||||
export const tier2LimitSet: LimitSet = {
|
||||
[FeatureId.SITES]: {
|
||||
value: 10,
|
||||
description: "Starter limit"
|
||||
}, // 50 sites up for 31 days
|
||||
[FeatureId.USERS]: {
|
||||
value: 150,
|
||||
description: "Starter limit"
|
||||
value: 100,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.SITES]: {
|
||||
value: 50,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.EGRESS_DATA_MB]: {
|
||||
value: 12000000,
|
||||
description: "Starter limit"
|
||||
}, // 12000 GB
|
||||
[FeatureId.DOMAINS]: {
|
||||
value: 250,
|
||||
description: "Starter limit"
|
||||
value: 50,
|
||||
description: "Team limit"
|
||||
},
|
||||
[FeatureId.REMOTE_EXIT_NODES]: {
|
||||
value: 5,
|
||||
description: "Starter limit"
|
||||
}
|
||||
value: 3,
|
||||
description: "Team limit"
|
||||
},
|
||||
};
|
||||
|
||||
export const tier3LimitSet: LimitSet = {
|
||||
[FeatureId.SITES]: {
|
||||
value: 10,
|
||||
description: "Scale limit"
|
||||
}, // 50 sites up for 31 days
|
||||
[FeatureId.USERS]: {
|
||||
value: 150,
|
||||
description: "Scale limit"
|
||||
value: 500,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.EGRESS_DATA_MB]: {
|
||||
value: 12000000,
|
||||
description: "Scale limit"
|
||||
}, // 12000 GB
|
||||
[FeatureId.DOMAINS]: {
|
||||
[FeatureId.SITES]: {
|
||||
value: 250,
|
||||
description: "Scale limit"
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.DOMAINS]: {
|
||||
value: 100,
|
||||
description: "Business limit"
|
||||
},
|
||||
[FeatureId.REMOTE_EXIT_NODES]: {
|
||||
value: 5,
|
||||
description: "Scale limit"
|
||||
}
|
||||
value: 20,
|
||||
description: "Business limit"
|
||||
},
|
||||
};
|
||||
|
||||
38
server/lib/billing/tierMatrix.ts
Normal file
38
server/lib/billing/tierMatrix.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export enum TierFeature {
|
||||
OrgOidc = "orgOidc",
|
||||
CustomAuthenticationDomain = "customAuthenticationDomain",
|
||||
DeviceApprovals = "deviceApprovals",
|
||||
LoginPageBranding = "loginPageBranding",
|
||||
LogExport = "logExport",
|
||||
AccessLogs = "accessLogs",
|
||||
ActionLogs = "actionLogs",
|
||||
RotateCredentials = "rotateCredentials",
|
||||
MaintencePage = "maintencePage",
|
||||
DevicePosture = "devicePosture",
|
||||
TwoFactorEnforcement = "twoFactorEnforcement",
|
||||
SessionDurationPolicies = "sessionDurationPolicies",
|
||||
PasswordExpirationPolicies = "passwordExpirationPolicies"
|
||||
}
|
||||
|
||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.CustomAuthenticationDomain]: [
|
||||
"tier1",
|
||||
"tier2",
|
||||
"tier3",
|
||||
"enterprise"
|
||||
],
|
||||
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
|
||||
[TierFeature.LoginPageBranding]: ["tier1", "tier3", "enterprise"],
|
||||
[TierFeature.LogExport]: ["tier3", "enterprise"],
|
||||
[TierFeature.AccessLogs]: ["tier2", "tier3", "enterprise"],
|
||||
[TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"],
|
||||
[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"]
|
||||
};
|
||||
@@ -517,7 +517,6 @@ export class UsageService {
|
||||
|
||||
public async checkLimitSet(
|
||||
orgId: string,
|
||||
kickSites = false,
|
||||
featureId?: FeatureId,
|
||||
usage?: Usage,
|
||||
trx: Transaction | typeof db = db
|
||||
@@ -591,58 +590,6 @@ export class UsageService {
|
||||
break; // Exit early if any limit is exceeded
|
||||
}
|
||||
}
|
||||
|
||||
// If any limits are exceeded, disconnect all sites for this organization
|
||||
if (hasExceededLimits && kickSites) {
|
||||
logger.warn(
|
||||
`Disconnecting all sites for org ${orgId} due to exceeded limits`
|
||||
);
|
||||
|
||||
// Get all sites for this organization
|
||||
const orgSites = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.orgId, orgId));
|
||||
|
||||
// Mark all sites as offline and send termination messages
|
||||
const siteUpdates = orgSites.map((site) => site.siteId);
|
||||
|
||||
if (siteUpdates.length > 0) {
|
||||
// Send termination messages to newt sites
|
||||
for (const site of orgSites) {
|
||||
if (site.type === "newt") {
|
||||
const [newt] = await trx
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
|
||||
if (newt) {
|
||||
const payload = {
|
||||
type: `newt/wg/terminate`,
|
||||
data: {
|
||||
reason: "Usage limits exceeded"
|
||||
}
|
||||
};
|
||||
|
||||
// Don't await to prevent blocking
|
||||
await sendToClient(newt.newtId, payload).catch(
|
||||
(error: any) => {
|
||||
logger.error(
|
||||
`Failed to send termination message to newt ${newt.newtId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error checking limits for org ${orgId}:`, error);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ import { resourcePassword } from "@server/db";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "../billing/tierMatrix";
|
||||
import { t } from "@faker-js/faker/dist/airline-DF6RqYmq";
|
||||
|
||||
export type ProxyResourcesResults = {
|
||||
proxyResource: Resource;
|
||||
@@ -212,7 +213,7 @@ export async function updateProxyResources(
|
||||
} else {
|
||||
// Update existing resource
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.maintencePage);
|
||||
if (!isLicensed) {
|
||||
resourceData.maintenance = undefined;
|
||||
}
|
||||
@@ -648,7 +649,7 @@ export async function updateProxyResources(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.maintencePage);
|
||||
if (!isLicensed) {
|
||||
resourceData.maintenance = undefined;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { sendTerminateClient } from "@server/routers/client/terminate";
|
||||
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";
|
||||
import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations";
|
||||
import { OlmErrorCodes } from "@server/routers/olm/error";
|
||||
import { tierMatrix } from "./billing/tierMatrix";
|
||||
|
||||
export async function calculateUserClientsForOrgs(
|
||||
userId: string,
|
||||
@@ -189,7 +190,8 @@ export async function calculateUserClientsForOrgs(
|
||||
const niceId = await getUniqueClientName(orgId);
|
||||
|
||||
const isOrgLicensed = await isLicensedOrSubscribed(
|
||||
userOrg.orgId
|
||||
userOrg.orgId,
|
||||
tierMatrix.deviceApprovals
|
||||
);
|
||||
const requireApproval =
|
||||
build !== "oss" &&
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export async function isLicensedOrSubscribed(
|
||||
orgId: string,
|
||||
tiers: Tier[]
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export async function isSubscribed(orgId: string): Promise<boolean> {
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export async function isSubscribed(
|
||||
orgId: string,
|
||||
tiers: Tier[]
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
|
||||
import { build } from "@server/build";
|
||||
import { db, customers, subscriptions } from "@server/db";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
import { eq, and, ne } from "drizzle-orm";
|
||||
|
||||
export async function getOrgTierData(
|
||||
orgId: string
|
||||
): Promise<{ tier: "tier1" | "tier2" | "tier3" | null; active: boolean }> {
|
||||
let tier: "tier1" | "tier2" | "tier3" | null = null;
|
||||
): Promise<{ tier: Tier | null; active: boolean }> {
|
||||
let tier: Tier | null = null;
|
||||
let active = false;
|
||||
|
||||
if (build !== "saas") {
|
||||
|
||||
@@ -78,6 +78,8 @@ export async function checkOrgAccessPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check that the org is subscribed
|
||||
|
||||
// get the needed data
|
||||
|
||||
if (!props.org) {
|
||||
|
||||
@@ -13,16 +13,19 @@
|
||||
|
||||
import { build } from "@server/build";
|
||||
import license from "#private/license/license";
|
||||
import { getOrgTierData } from "#private/lib/billing";
|
||||
import { isSubscribed } from "#private/lib/isSubscribed";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
|
||||
export async function isLicensedOrSubscribed(
|
||||
orgId: string,
|
||||
tiers: Tier[]
|
||||
): 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;
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
|
||||
import { build } from "@server/build";
|
||||
import { getOrgTierData } from "#private/lib/billing";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export async function isSubscribed(orgId: string): Promise<boolean> {
|
||||
export async function isSubscribed(
|
||||
orgId: string,
|
||||
tiers: Tier[]
|
||||
): 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;
|
||||
|
||||
@@ -16,45 +16,61 @@ import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { build } from "@server/build";
|
||||
import { getOrgTierData } from "#private/lib/billing";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export function verifyValidSubscription(tiers: Tier[]) {
|
||||
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 (!active) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Organization does not have an active subscription"
|
||||
)
|
||||
);
|
||||
}
|
||||
if (!isTier) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Organization subscription tier does not have access to this feature"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function verifyValidSubscription(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
if (build != "saas") {
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -206,7 +206,8 @@ export async function changeTier(
|
||||
// Keep the existing item unchanged if we can't find it
|
||||
return {
|
||||
id: stripeItem.id,
|
||||
price: stripeItem.price.id
|
||||
price: stripeItem.price.id,
|
||||
quantity: stripeItem.quantity
|
||||
};
|
||||
}
|
||||
|
||||
@@ -216,14 +217,16 @@ export async function changeTier(
|
||||
if (newPriceId) {
|
||||
return {
|
||||
id: stripeItem.id,
|
||||
price: newPriceId
|
||||
price: newPriceId,
|
||||
quantity: stripeItem.quantity
|
||||
};
|
||||
}
|
||||
|
||||
// If no mapping found, keep existing
|
||||
return {
|
||||
id: stripeItem.id,
|
||||
price: stripeItem.price.id
|
||||
price: stripeItem.price.id,
|
||||
quantity: stripeItem.quantity
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -20,8 +20,9 @@ import {
|
||||
getScaleFeaturePriceSet,
|
||||
} from "@server/lib/billing/features";
|
||||
import Stripe from "stripe";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export type SubscriptionType = "tier1" | "tier2" | "tier3" | "license";
|
||||
export type SubscriptionType = Tier | "license";
|
||||
|
||||
export function getSubType(fullSubscription: Stripe.Response<Stripe.Subscription>): SubscriptionType | null {
|
||||
// Determine subscription type by checking subscription items
|
||||
|
||||
@@ -31,6 +31,7 @@ import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses";
|
||||
import { sendEmail } from "@server/emails";
|
||||
import EnterpriseEditionKeyGenerated from "@server/emails/templates/EnterpriseEditionKeyGenerated";
|
||||
import config from "@server/lib/config";
|
||||
import { getFeatureIdByPriceId } from "@server/lib/billing/features";
|
||||
|
||||
export async function handleSubscriptionCreated(
|
||||
subscription: Stripe.Subscription
|
||||
@@ -91,11 +92,15 @@ export async function handleSubscriptionCreated(
|
||||
name = product.name || null;
|
||||
}
|
||||
|
||||
// Get the feature ID from the price ID
|
||||
const featureId = getFeatureIdByPriceId(item.price.id);
|
||||
|
||||
return {
|
||||
stripeSubscriptionItemId: item.id,
|
||||
subscriptionId: subscription.id,
|
||||
planId: item.plan.id,
|
||||
priceId: item.price.id,
|
||||
featureId: featureId || null,
|
||||
meterId: item.plan.meter,
|
||||
unitAmount: item.price.unit_amount || 0,
|
||||
currentPeriodStart: item.current_period_start,
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { getFeatureIdByMetricId } from "@server/lib/billing/features";
|
||||
import { getFeatureIdByMetricId, getFeatureIdByPriceId } from "@server/lib/billing/features";
|
||||
import stripe from "#private/lib/stripe";
|
||||
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
|
||||
import { getSubType } from "./getSubType";
|
||||
@@ -81,20 +81,40 @@ export async function handleSubscriptionUpdated(
|
||||
|
||||
// Upsert subscription items
|
||||
if (Array.isArray(fullSubscription.items?.data)) {
|
||||
const itemsToUpsert = fullSubscription.items.data.map((item) => ({
|
||||
stripeSubscriptionItemId: item.id,
|
||||
subscriptionId: subscription.id,
|
||||
planId: item.plan.id,
|
||||
priceId: item.price.id,
|
||||
meterId: item.plan.meter,
|
||||
unitAmount: item.price.unit_amount || 0,
|
||||
currentPeriodStart: item.current_period_start,
|
||||
currentPeriodEnd: item.current_period_end,
|
||||
tiers: item.price.tiers
|
||||
? JSON.stringify(item.price.tiers)
|
||||
: null,
|
||||
interval: item.plan.interval
|
||||
}));
|
||||
// First, get existing items to preserve featureId when there's no match
|
||||
const existingItems = await db
|
||||
.select()
|
||||
.from(subscriptionItems)
|
||||
.where(eq(subscriptionItems.subscriptionId, subscription.id));
|
||||
|
||||
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(
|
||||
(ei) => ei.stripeSubscriptionItemId === item.id
|
||||
);
|
||||
featureId = existingItem?.featureId || null;
|
||||
}
|
||||
|
||||
return {
|
||||
stripeSubscriptionItemId: item.id,
|
||||
subscriptionId: subscription.id,
|
||||
planId: item.plan.id,
|
||||
priceId: item.price.id,
|
||||
featureId: featureId,
|
||||
meterId: item.plan.meter,
|
||||
unitAmount: item.price.unit_amount || 0,
|
||||
currentPeriodStart: item.current_period_start,
|
||||
currentPeriodEnd: item.current_period_end,
|
||||
tiers: item.price.tiers
|
||||
? JSON.stringify(item.price.tiers)
|
||||
: null,
|
||||
interval: item.plan.interval
|
||||
};
|
||||
});
|
||||
if (itemsToUpsert.length > 0) {
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import {
|
||||
freeLimitSet,
|
||||
homeLabLimitSet,
|
||||
tier1LimitSet,
|
||||
tier2LimitSet,
|
||||
tier3LimitSet,
|
||||
limitsService,
|
||||
@@ -22,10 +22,12 @@ import {
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { SubscriptionType } from "./hooks/getSubType";
|
||||
|
||||
function getLimitSetForSubscriptionType(subType: SubscriptionType | null): LimitSet {
|
||||
function getLimitSetForSubscriptionType(
|
||||
subType: SubscriptionType | null
|
||||
): LimitSet {
|
||||
switch (subType) {
|
||||
case "tier1":
|
||||
return homeLabLimitSet;
|
||||
return tier1LimitSet;
|
||||
case "tier2":
|
||||
return tier2LimitSet;
|
||||
case "tier3":
|
||||
@@ -48,12 +50,12 @@ export async function handleSubscriptionLifesycle(
|
||||
case "active":
|
||||
const activeLimitSet = getLimitSetForSubscriptionType(subType);
|
||||
await limitsService.applyLimitSetToOrg(orgId, activeLimitSet);
|
||||
await usageService.checkLimitSet(orgId, true);
|
||||
await usageService.checkLimitSet(orgId);
|
||||
break;
|
||||
case "canceled":
|
||||
// Subscription canceled - revert to free tier
|
||||
await limitsService.applyLimitSetToOrg(orgId, freeLimitSet);
|
||||
await usageService.checkLimitSet(orgId, true);
|
||||
await usageService.checkLimitSet(orgId);
|
||||
break;
|
||||
case "past_due":
|
||||
// Payment past due - keep current limits but notify customer
|
||||
@@ -62,7 +64,7 @@ export async function handleSubscriptionLifesycle(
|
||||
case "unpaid":
|
||||
// Subscription unpaid - revert to free tier
|
||||
await limitsService.applyLimitSetToOrg(orgId, freeLimitSet);
|
||||
await usageService.checkLimitSet(orgId, true);
|
||||
await usageService.checkLimitSet(orgId);
|
||||
break;
|
||||
case "incomplete":
|
||||
// Payment incomplete - give them time to complete payment
|
||||
@@ -70,7 +72,7 @@ export async function handleSubscriptionLifesycle(
|
||||
case "incomplete_expired":
|
||||
// Payment never completed - revert to free tier
|
||||
await limitsService.applyLimitSetToOrg(orgId, freeLimitSet);
|
||||
await usageService.checkLimitSet(orgId, true);
|
||||
await usageService.checkLimitSet(orgId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
authenticated as a,
|
||||
authRouter as aa
|
||||
} from "@server/routers/external";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export const authenticated = a;
|
||||
export const unauthenticated = ua;
|
||||
@@ -76,7 +77,7 @@ unauthenticated.post(
|
||||
authenticated.put(
|
||||
"/org/:orgId/idp/oidc",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.orgOidc),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createIdp),
|
||||
logActionAudit(ActionsEnum.createIdp),
|
||||
@@ -86,7 +87,7 @@ authenticated.put(
|
||||
authenticated.post(
|
||||
"/org/:orgId/idp/:idpId/oidc",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.orgOidc),
|
||||
verifyOrgAccess,
|
||||
verifyIdpAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateIdp),
|
||||
@@ -279,7 +280,7 @@ authenticated.delete(
|
||||
authenticated.put(
|
||||
"/org/:orgId/login-page",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.customAuthenticationDomain),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createLoginPage),
|
||||
logActionAudit(ActionsEnum.createLoginPage),
|
||||
@@ -289,7 +290,7 @@ authenticated.put(
|
||||
authenticated.post(
|
||||
"/org/:orgId/login-page/:loginPageId",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.customAuthenticationDomain),
|
||||
verifyOrgAccess,
|
||||
verifyLoginPageAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateLoginPage),
|
||||
@@ -318,7 +319,7 @@ authenticated.get(
|
||||
authenticated.get(
|
||||
"/org/:orgId/approvals",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.deviceApprovals),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.listApprovals),
|
||||
logActionAudit(ActionsEnum.listApprovals),
|
||||
@@ -335,7 +336,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/org/:orgId/approvals/:approvalId",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.deviceApprovals),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateApprovals),
|
||||
logActionAudit(ActionsEnum.updateApprovals),
|
||||
@@ -345,7 +346,7 @@ authenticated.put(
|
||||
authenticated.get(
|
||||
"/org/:orgId/login-page-branding",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.loginPageBranding),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.getLoginPage),
|
||||
logActionAudit(ActionsEnum.getLoginPage),
|
||||
@@ -355,7 +356,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/org/:orgId/login-page-branding",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.loginPageBranding),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateLoginPage),
|
||||
logActionAudit(ActionsEnum.updateLoginPage),
|
||||
@@ -365,7 +366,6 @@ authenticated.put(
|
||||
authenticated.delete(
|
||||
"/org/:orgId/login-page-branding",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteLoginPage),
|
||||
logActionAudit(ActionsEnum.deleteLoginPage),
|
||||
@@ -433,7 +433,7 @@ authenticated.post(
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/action",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.actionLogs),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.exportLogs),
|
||||
logs.queryActionAuditLogs
|
||||
@@ -442,7 +442,7 @@ authenticated.get(
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/action/export",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.logExport),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.exportLogs),
|
||||
logActionAudit(ActionsEnum.exportLogs),
|
||||
@@ -452,7 +452,7 @@ authenticated.get(
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/access",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.accessLogs),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.exportLogs),
|
||||
logs.queryAccessAuditLogs
|
||||
@@ -461,7 +461,7 @@ authenticated.get(
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/access/export",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.logExport),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.exportLogs),
|
||||
logActionAudit(ActionsEnum.exportLogs),
|
||||
@@ -472,7 +472,7 @@ authenticated.post(
|
||||
"/re-key/:clientId/regenerate-client-secret",
|
||||
verifyClientAccess, // this is first to set the org id
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.rotateCredentials),
|
||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||
reKey.reGenerateClientSecret
|
||||
);
|
||||
@@ -481,7 +481,7 @@ authenticated.post(
|
||||
"/re-key/:siteId/regenerate-site-secret",
|
||||
verifySiteAccess, // this is first to set the org id
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.rotateCredentials),
|
||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||
reKey.reGenerateSiteSecret
|
||||
);
|
||||
@@ -489,7 +489,7 @@ authenticated.post(
|
||||
authenticated.put(
|
||||
"/re-key/:orgId/regenerate-remote-exit-node-secret",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.rotateCredentials),
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||
reKey.reGenerateExitNodeSecret
|
||||
|
||||
@@ -85,7 +85,7 @@ export async function createRemoteExitNode(
|
||||
if (usage) {
|
||||
const rejectRemoteExitNodes = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
false,
|
||||
|
||||
FeatureId.REMOTE_EXIT_NODES,
|
||||
{
|
||||
...usage,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { getUserDeviceName } from "@server/db/names";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const getClientSchema = z.strictObject({
|
||||
clientId: z
|
||||
@@ -56,19 +57,29 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
||||
}
|
||||
|
||||
type PostureData = {
|
||||
biometricsEnabled?: boolean | null;
|
||||
diskEncrypted?: boolean | null;
|
||||
firewallEnabled?: boolean | null;
|
||||
autoUpdatesEnabled?: boolean | null;
|
||||
tpmAvailable?: boolean | null;
|
||||
windowsAntivirusEnabled?: boolean | null;
|
||||
macosSipEnabled?: boolean | null;
|
||||
macosGatekeeperEnabled?: boolean | null;
|
||||
macosFirewallStealthMode?: boolean | null;
|
||||
linuxAppArmorEnabled?: boolean | null;
|
||||
linuxSELinuxEnabled?: boolean | null;
|
||||
biometricsEnabled?: boolean | null | "-";
|
||||
diskEncrypted?: boolean | null | "-";
|
||||
firewallEnabled?: boolean | null | "-";
|
||||
autoUpdatesEnabled?: boolean | null | "-";
|
||||
tpmAvailable?: boolean | null | "-";
|
||||
windowsAntivirusEnabled?: boolean | null | "-";
|
||||
macosSipEnabled?: boolean | null | "-";
|
||||
macosGatekeeperEnabled?: boolean | null | "-";
|
||||
macosFirewallStealthMode?: boolean | null | "-";
|
||||
linuxAppArmorEnabled?: boolean | null | "-";
|
||||
linuxSELinuxEnabled?: boolean | null | "-";
|
||||
};
|
||||
|
||||
function maskPostureDataWithPlaceholder(posture: PostureData): PostureData {
|
||||
const masked: PostureData = {};
|
||||
for (const key of Object.keys(posture) as (keyof PostureData)[]) {
|
||||
if (posture[key] !== undefined && posture[key] !== null) {
|
||||
(masked as Record<keyof PostureData, "-">)[key] = "-";
|
||||
}
|
||||
}
|
||||
return masked;
|
||||
}
|
||||
|
||||
function getPlatformPostureData(
|
||||
platform: string | null | undefined,
|
||||
fingerprint: typeof currentFingerprint.$inferSelect | null
|
||||
@@ -284,9 +295,11 @@ export async function getClient(
|
||||
);
|
||||
}
|
||||
|
||||
const isUserDevice = client.user !== null && client.user !== undefined;
|
||||
|
||||
// Replace name with device name if OLM exists
|
||||
let clientName = client.clients.name;
|
||||
if (client.olms) {
|
||||
if (client.olms && isUserDevice) {
|
||||
const model = client.currentFingerprint?.deviceModel || null;
|
||||
clientName = getUserDeviceName(model, client.clients.name);
|
||||
}
|
||||
@@ -294,32 +307,35 @@ export async function getClient(
|
||||
// Build fingerprint data if available
|
||||
const fingerprintData = client.currentFingerprint
|
||||
? {
|
||||
username: client.currentFingerprint.username || null,
|
||||
hostname: client.currentFingerprint.hostname || null,
|
||||
platform: client.currentFingerprint.platform || null,
|
||||
osVersion: client.currentFingerprint.osVersion || null,
|
||||
kernelVersion:
|
||||
client.currentFingerprint.kernelVersion || null,
|
||||
arch: client.currentFingerprint.arch || null,
|
||||
deviceModel: client.currentFingerprint.deviceModel || null,
|
||||
serialNumber: client.currentFingerprint.serialNumber || null,
|
||||
firstSeen: client.currentFingerprint.firstSeen || null,
|
||||
lastSeen: client.currentFingerprint.lastSeen || null
|
||||
}
|
||||
username: client.currentFingerprint.username || null,
|
||||
hostname: client.currentFingerprint.hostname || null,
|
||||
platform: client.currentFingerprint.platform || null,
|
||||
osVersion: client.currentFingerprint.osVersion || null,
|
||||
kernelVersion:
|
||||
client.currentFingerprint.kernelVersion || null,
|
||||
arch: client.currentFingerprint.arch || null,
|
||||
deviceModel: client.currentFingerprint.deviceModel || null,
|
||||
serialNumber: client.currentFingerprint.serialNumber || null,
|
||||
firstSeen: client.currentFingerprint.firstSeen || null,
|
||||
lastSeen: client.currentFingerprint.lastSeen || null
|
||||
}
|
||||
: null;
|
||||
|
||||
// Build posture data if available (platform-specific)
|
||||
// Only return posture data if org is licensed/subscribed
|
||||
let postureData: PostureData | null = null;
|
||||
const isOrgLicensed = await isLicensedOrSubscribed(
|
||||
client.clients.orgId
|
||||
// Licensed: real values; not licensed: same keys but values set to "-"
|
||||
const rawPosture = getPlatformPostureData(
|
||||
client.currentFingerprint?.platform || null,
|
||||
client.currentFingerprint
|
||||
);
|
||||
if (isOrgLicensed) {
|
||||
postureData = getPlatformPostureData(
|
||||
client.currentFingerprint?.platform || null,
|
||||
client.currentFingerprint
|
||||
);
|
||||
}
|
||||
const isOrgLicensed = await isLicensedOrSubscribed(
|
||||
client.clients.orgId,
|
||||
tierMatrix.devicePosture
|
||||
);
|
||||
const postureData: PostureData | null = rawPosture
|
||||
? isOrgLicensed
|
||||
? rawPosture
|
||||
: maskPostureDataWithPlaceholder(rawPosture)
|
||||
: null;
|
||||
|
||||
const data: GetClientResponse = {
|
||||
...client.clients,
|
||||
|
||||
@@ -320,7 +320,10 @@ export async function listClients(
|
||||
// Merge clients with their site associations and replace name with device name
|
||||
const clientsWithSites = clientsList.map((client) => {
|
||||
const model = client.deviceModel || null;
|
||||
const newName = getUserDeviceName(model, client.name);
|
||||
let newName = client.name;
|
||||
if (filter === "user") {
|
||||
newName = getUserDeviceName(model, client.name);
|
||||
}
|
||||
return {
|
||||
...client,
|
||||
name: newName,
|
||||
|
||||
@@ -131,7 +131,7 @@ export async function createOrgDomain(
|
||||
}
|
||||
const rejectDomains = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
false,
|
||||
|
||||
FeatureId.DOMAINS,
|
||||
{
|
||||
...usage,
|
||||
|
||||
@@ -178,11 +178,9 @@ export async function updateSiteBandwidth(
|
||||
|
||||
// Process usage updates outside of site update transactions
|
||||
// This separates the concerns and reduces lock contention
|
||||
if (calcUsageAndLimits && (orgUsageMap.size > 0)) {
|
||||
if (calcUsageAndLimits && orgUsageMap.size > 0) {
|
||||
// Sort org IDs to ensure consistent lock ordering
|
||||
const allOrgIds = [
|
||||
...new Set([...orgUsageMap.keys()])
|
||||
].sort();
|
||||
const allOrgIds = [...new Set([...orgUsageMap.keys()])].sort();
|
||||
|
||||
for (const orgId of allOrgIds) {
|
||||
try {
|
||||
@@ -199,7 +197,7 @@ export async function updateSiteBandwidth(
|
||||
usageService
|
||||
.checkLimitSet(
|
||||
orgId,
|
||||
true,
|
||||
|
||||
FeatureId.EGRESS_DATA_MB,
|
||||
bandwidthUsage
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { db, ExitNode, exitNodeOrgs, newts, Transaction } from "@server/db";
|
||||
import { db, ExitNode, newts, Transaction } from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db";
|
||||
import { targetHealthCheck } from "@server/db";
|
||||
import { eq, and, sql, inArray, ne } from "drizzle-orm";
|
||||
import { exitNodes, Newt, sites } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { addPeer, deletePeer } from "../gerbil/peers";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import {
|
||||
findNextAvailableCidr,
|
||||
getNextAvailableClientSubnet
|
||||
} from "@server/lib/ip";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { FeatureId } from "@server/lib/billing";
|
||||
import {
|
||||
selectBestExitNode,
|
||||
verifyExitNodeOrgAccess
|
||||
@@ -30,8 +26,6 @@ export type ExitNodePingResult = {
|
||||
wasPreviouslyConnected: boolean;
|
||||
};
|
||||
|
||||
const numTimesLimitExceededForId: Record<string, number> = {};
|
||||
|
||||
export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
const { message, client, sendToClient } = context;
|
||||
const newt = client as Newt;
|
||||
@@ -96,42 +90,6 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
fetchContainers(newt.newtId);
|
||||
}
|
||||
|
||||
const rejectSites = await usageService.checkLimitSet(
|
||||
oldSite.orgId,
|
||||
false,
|
||||
FeatureId.SITES
|
||||
);
|
||||
const rejectEgressDataMb = await usageService.checkLimitSet(
|
||||
oldSite.orgId,
|
||||
false,
|
||||
FeatureId.EGRESS_DATA_MB
|
||||
);
|
||||
|
||||
// Do we need to check the users and domains count limits here?
|
||||
// const rejectUsers = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.USERS);
|
||||
// const rejectDomains = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.DOMAINS);
|
||||
|
||||
// if (rejectEgressDataMb || rejectSites || rejectUsers || rejectDomains) {
|
||||
if (rejectEgressDataMb || rejectSites) {
|
||||
logger.info(
|
||||
`Usage limits exceeded for org ${oldSite.orgId}. Rejecting newt registration.`
|
||||
);
|
||||
|
||||
// PREVENT FURTHER REGISTRATION ATTEMPTS SO WE DON'T SPAM
|
||||
|
||||
// Increment the limit exceeded count for this site
|
||||
numTimesLimitExceededForId[newt.newtId] =
|
||||
(numTimesLimitExceededForId[newt.newtId] || 0) + 1;
|
||||
|
||||
if (numTimesLimitExceededForId[newt.newtId] > 15) {
|
||||
logger.debug(
|
||||
`Newt ${newt.newtId} has exceeded usage limits 15 times. Terminating...`
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let siteSubnet = oldSite.subnet;
|
||||
let exitNodeIdToQuery = oldSite.exitNodeId;
|
||||
if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) {
|
||||
|
||||
@@ -117,6 +117,8 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
|
||||
|
||||
try {
|
||||
// get the client
|
||||
const [client] = await db
|
||||
@@ -219,7 +221,9 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
||||
logger.error("Error handling ping message", { error });
|
||||
}
|
||||
|
||||
await handleFingerprintInsertion(olm, fingerprint, postures);
|
||||
if (isUserDevice) {
|
||||
await handleFingerprintInsertion(olm, fingerprint, postures);
|
||||
}
|
||||
|
||||
return {
|
||||
message: {
|
||||
|
||||
@@ -53,7 +53,11 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
postures
|
||||
});
|
||||
|
||||
await handleFingerprintInsertion(olm, fingerprint, postures);
|
||||
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
|
||||
|
||||
if (isUserDevice) {
|
||||
await handleFingerprintInsertion(olm, fingerprint, postures);
|
||||
}
|
||||
|
||||
if (
|
||||
(olmVersion && olm.version !== olmVersion) ||
|
||||
|
||||
@@ -12,7 +12,8 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { build } from "@server/build";
|
||||
import { cache } from "@server/lib/cache";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
||||
|
||||
const updateOrgParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -87,26 +88,83 @@ export async function updateOrg(
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (!isLicensed) {
|
||||
// Check 2FA enforcement feature
|
||||
const has2FAFeature = await isLicensedOrSubscribed(
|
||||
orgId,
|
||||
tierMatrix[TierFeature.TwoFactorEnforcement]
|
||||
);
|
||||
if (!has2FAFeature) {
|
||||
parsedBody.data.requireTwoFactor = undefined;
|
||||
parsedBody.data.maxSessionLengthHours = undefined;
|
||||
parsedBody.data.passwordExpiryDays = undefined;
|
||||
}
|
||||
|
||||
const subscribed = await isSubscribed(orgId);
|
||||
if (
|
||||
build == "saas" &&
|
||||
subscribed &&
|
||||
parsedBody.data.settingsLogRetentionDaysRequest &&
|
||||
parsedBody.data.settingsLogRetentionDaysRequest > 30
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"You are not allowed to set log retention days greater than 30 with your current subscription"
|
||||
)
|
||||
);
|
||||
// Check session duration policies feature
|
||||
const hasSessionDurationFeature = await isLicensedOrSubscribed(
|
||||
orgId,
|
||||
tierMatrix[TierFeature.SessionDurationPolicies]
|
||||
);
|
||||
if (!hasSessionDurationFeature) {
|
||||
parsedBody.data.maxSessionLengthHours = undefined;
|
||||
}
|
||||
|
||||
// Check password expiration policies feature
|
||||
const hasPasswordExpirationFeature = await isLicensedOrSubscribed(
|
||||
orgId,
|
||||
tierMatrix[TierFeature.PasswordExpirationPolicies]
|
||||
);
|
||||
if (!hasPasswordExpirationFeature) {
|
||||
parsedBody.data.passwordExpiryDays = undefined;
|
||||
}
|
||||
if (build == "saas") {
|
||||
const { tier } = await getOrgTierData(orgId);
|
||||
|
||||
// Determine max allowed retention days based on tier
|
||||
let maxRetentionDays: number | null = null;
|
||||
if (!tier) {
|
||||
maxRetentionDays = 0;
|
||||
} else if (tier === "tier1") {
|
||||
maxRetentionDays = 7;
|
||||
} else if (tier === "tier2") {
|
||||
maxRetentionDays = 30;
|
||||
} else if (tier === "tier3") {
|
||||
maxRetentionDays = 90;
|
||||
}
|
||||
// For enterprise tier, no check (maxRetentionDays remains null)
|
||||
|
||||
if (maxRetentionDays !== null) {
|
||||
if (
|
||||
parsedBody.data.settingsLogRetentionDaysRequest !== undefined &&
|
||||
parsedBody.data.settingsLogRetentionDaysRequest > maxRetentionDays
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription`
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
parsedBody.data.settingsLogRetentionDaysAccess !== undefined &&
|
||||
parsedBody.data.settingsLogRetentionDaysAccess > maxRetentionDays
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription`
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
parsedBody.data.settingsLogRetentionDaysAction !== undefined &&
|
||||
parsedBody.data.settingsLogRetentionDaysAction > maxRetentionDays
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
`You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedOrg = await db
|
||||
|
||||
@@ -140,7 +140,7 @@ export async function createSite(
|
||||
}
|
||||
const rejectSites = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
false,
|
||||
|
||||
FeatureId.SITES,
|
||||
{
|
||||
...usage,
|
||||
|
||||
@@ -94,7 +94,10 @@ export async function acceptInvite(
|
||||
}
|
||||
|
||||
if (build == "saas") {
|
||||
const usage = await usageService.getUsage(existingInvite.orgId, FeatureId.USERS);
|
||||
const usage = await usageService.getUsage(
|
||||
existingInvite.orgId,
|
||||
FeatureId.USERS
|
||||
);
|
||||
if (!usage) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -105,7 +108,7 @@ export async function acceptInvite(
|
||||
}
|
||||
const rejectUsers = await usageService.checkLimitSet(
|
||||
existingInvite.orgId,
|
||||
false,
|
||||
|
||||
FeatureId.USERS,
|
||||
{
|
||||
...usage,
|
||||
@@ -163,7 +166,9 @@ export async function acceptInvite(
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.orgId, existingInvite.orgId));
|
||||
|
||||
logger.debug(`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}. Total users in org: ${totalUsers.length}`);
|
||||
logger.debug(
|
||||
`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}. Total users in org: ${totalUsers.length}`
|
||||
);
|
||||
});
|
||||
|
||||
if (totalUsers) {
|
||||
|
||||
@@ -21,11 +21,7 @@ const paramsSchema = z.strictObject({
|
||||
});
|
||||
|
||||
const bodySchema = z.strictObject({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.toLowerCase()
|
||||
.optional(),
|
||||
email: z.string().email().toLowerCase().optional(),
|
||||
username: z.string().nonempty().toLowerCase(),
|
||||
name: z.string().optional(),
|
||||
type: z.enum(["internal", "oidc"]).optional(),
|
||||
@@ -94,7 +90,7 @@ export async function createOrgUser(
|
||||
}
|
||||
const rejectUsers = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
false,
|
||||
|
||||
FeatureId.USERS,
|
||||
{
|
||||
...usage,
|
||||
|
||||
@@ -133,7 +133,6 @@ export async function inviteUser(
|
||||
}
|
||||
const rejectUsers = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
false,
|
||||
FeatureId.USERS,
|
||||
{
|
||||
...usage,
|
||||
|
||||
1
server/types/Tiers.ts
Normal file
1
server/types/Tiers.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Tier = "tier1" | "tier2" | "tier3" | "enterprise";
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
} from "@server/routers/billing/types";
|
||||
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";
|
||||
|
||||
// Plan tier definitions matching the mockup
|
||||
type PlanId = "free" | "homelab" | "team" | "business" | "enterprise";
|
||||
@@ -58,7 +60,7 @@ interface PlanOption {
|
||||
name: string;
|
||||
price: string;
|
||||
priceDetail?: string;
|
||||
tierType: "tier1" | "tier2" | "tier3" | null; // Maps to backend tier types
|
||||
tierType: Tier | null;
|
||||
}
|
||||
|
||||
// Tier limits for display in confirmation dialog
|
||||
@@ -69,7 +71,7 @@ interface TierLimits {
|
||||
remoteNodes: number;
|
||||
}
|
||||
|
||||
const tierLimits: Record<"tier1" | "tier2" | "tier3", TierLimits> = {
|
||||
const tierLimits: Record<Tier, TierLimits> = {
|
||||
tier1: {
|
||||
sites: 3,
|
||||
users: 3,
|
||||
@@ -155,7 +157,7 @@ export default function BillingPage() {
|
||||
const [hasSubscription, setHasSubscription] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentTier, setCurrentTier] = useState<
|
||||
"tier1" | "tier2" | "tier3" | null
|
||||
Tier | null
|
||||
>(null);
|
||||
|
||||
// Usage IDs
|
||||
@@ -167,7 +169,7 @@ export default function BillingPage() {
|
||||
// Confirmation dialog state
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [pendingTier, setPendingTier] = useState<{
|
||||
tier: "tier1" | "tier2" | "tier3";
|
||||
tier: Tier,
|
||||
action: "upgrade" | "downgrade";
|
||||
planName: string;
|
||||
price: string;
|
||||
@@ -194,10 +196,7 @@ export default function BillingPage() {
|
||||
|
||||
if (tierSub?.subscription) {
|
||||
setCurrentTier(
|
||||
tierSub.subscription.type as
|
||||
| "tier1"
|
||||
| "tier2"
|
||||
| "tier3"
|
||||
tierSub.subscription.type as Tier
|
||||
);
|
||||
setHasSubscription(
|
||||
tierSub.subscription.status === "active"
|
||||
@@ -243,7 +242,7 @@ export default function BillingPage() {
|
||||
}, [org.org.orgId]);
|
||||
|
||||
const handleStartSubscription = async (
|
||||
tier: "tier1" | "tier2" | "tier3"
|
||||
tier: Tier
|
||||
) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -300,7 +299,7 @@ export default function BillingPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeTier = async (tier: "tier1" | "tier2" | "tier3") => {
|
||||
const handleChangeTier = async (tier: Tier) => {
|
||||
if (!hasSubscription) {
|
||||
// If no subscription, start a new one
|
||||
handleStartSubscription(tier);
|
||||
@@ -343,7 +342,7 @@ export default function BillingPage() {
|
||||
};
|
||||
|
||||
const showTierConfirmation = (
|
||||
tier: "tier1" | "tier2" | "tier3",
|
||||
tier: Tier,
|
||||
action: "upgrade" | "downgrade",
|
||||
planName: string,
|
||||
price: string
|
||||
@@ -453,8 +452,19 @@ export default function BillingPage() {
|
||||
// Calculate current usage cost for display
|
||||
const getUserCount = () => getUsageValue(USERS);
|
||||
const getPricePerUser = () => {
|
||||
if (currentTier === "tier2") return 5;
|
||||
if (currentTier === "tier3") return 10;
|
||||
console.log("Calculating price per user, tierSubscription:", tierSubscription);
|
||||
if (!tierSubscription?.items) return 0;
|
||||
|
||||
// Find the subscription item for USERS feature
|
||||
const usersItem = tierSubscription.items.find(
|
||||
(item) => item.planId === USERS
|
||||
);
|
||||
|
||||
// unitAmount is in cents, convert to dollars
|
||||
if (usersItem?.unitAmount) {
|
||||
return usersItem.unitAmount / 100;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -51,6 +52,7 @@ export default function Page() {
|
||||
>("role");
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const params = useParams();
|
||||
|
||||
@@ -806,7 +808,7 @@ export default function Page() {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createLoading}
|
||||
disabled={createLoading || !isPaidUser}
|
||||
loading={createLoading}
|
||||
onClick={() => {
|
||||
// log any issues with the form
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { build } from "@server/build";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{}>;
|
||||
}
|
||||
|
||||
export default async function Layout(props: LayoutProps) {
|
||||
const env = pullEnv();
|
||||
|
||||
if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return props.children;
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function CredentialsPage() {
|
||||
</Alert>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
{build !== "oss" && (
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -61,7 +61,9 @@ export default function CredentialsPage() {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
return (
|
||||
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
|
||||
);
|
||||
};
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
@@ -181,7 +183,7 @@ export default function CredentialsPage() {
|
||||
</Alert>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
{build !== "oss" && (
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -28,10 +28,19 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useEffect, useTransition } from "react";
|
||||
import { Check, Ban, Shield, ShieldOff, Clock, CheckCircle2, XCircle } from "lucide-react";
|
||||
import {
|
||||
Check,
|
||||
Ban,
|
||||
Shield,
|
||||
ShieldOff,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
XCircle
|
||||
} from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
||||
import { SiAndroid } from "react-icons/si";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
function formatTimestamp(timestamp: number | null | undefined): string {
|
||||
if (!timestamp) return "-";
|
||||
@@ -111,13 +120,13 @@ function getPlatformFieldConfig(
|
||||
osVersion: { show: true, labelKey: "iosVersion" },
|
||||
kernelVersion: { show: false, labelKey: "kernelVersion" },
|
||||
arch: { show: true, labelKey: "architecture" },
|
||||
deviceModel: { show: true, labelKey: "deviceModel" },
|
||||
deviceModel: { show: true, labelKey: "deviceModel" }
|
||||
},
|
||||
android: {
|
||||
osVersion: { show: true, labelKey: "androidVersion" },
|
||||
kernelVersion: { show: true, labelKey: "kernelVersion" },
|
||||
arch: { show: true, labelKey: "architecture" },
|
||||
deviceModel: { show: true, labelKey: "deviceModel" },
|
||||
deviceModel: { show: true, labelKey: "deviceModel" }
|
||||
},
|
||||
unknown: {
|
||||
osVersion: { show: true, labelKey: "osVersion" },
|
||||
@@ -133,7 +142,6 @@ function getPlatformFieldConfig(
|
||||
return configs[normalizedPlatform] || configs.unknown;
|
||||
}
|
||||
|
||||
|
||||
export default function GeneralPage() {
|
||||
const { client, updateClient } = useClientContext();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
@@ -145,11 +153,15 @@ 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;
|
||||
|
||||
const formatPostureValue = (value: boolean | null | undefined) => {
|
||||
if (value === null || value === undefined) return "-";
|
||||
const formatPostureValue = (
|
||||
value: boolean | null | undefined | "-"
|
||||
) => {
|
||||
if (value === null || value === undefined || value === "-")
|
||||
return "-";
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{value ? (
|
||||
@@ -423,7 +435,8 @@ export default function GeneralPage() {
|
||||
{t(
|
||||
fieldConfig
|
||||
.osVersion
|
||||
?.labelKey || "osVersion"
|
||||
?.labelKey ||
|
||||
"osVersion"
|
||||
)}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
@@ -559,8 +572,7 @@ export default function GeneralPage() {
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{/* Device Security Section */}
|
||||
{build !== "oss" && (
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
@@ -572,20 +584,24 @@ export default function GeneralPage() {
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
{client.posture && Object.keys(client.posture).length > 0 ? (
|
||||
<PaidFeaturesAlert />
|
||||
{client.posture &&
|
||||
Object.keys(client.posture).length > 0 ? (
|
||||
<>
|
||||
{!isPaidUser && <PaidFeaturesAlert />}
|
||||
<InfoSections cols={3}>
|
||||
{client.posture.biometricsEnabled !== null &&
|
||||
client.posture.biometricsEnabled !== undefined && (
|
||||
{client.posture.biometricsEnabled !==
|
||||
null &&
|
||||
client.posture.biometricsEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("biometricsEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(tierMatrix.devicePosture)
|
||||
? formatPostureValue(
|
||||
client.posture.biometricsEnabled
|
||||
client.posture
|
||||
.biometricsEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
@@ -593,7 +609,8 @@ export default function GeneralPage() {
|
||||
)}
|
||||
|
||||
{client.posture.diskEncrypted !== null &&
|
||||
client.posture.diskEncrypted !== undefined && (
|
||||
client.posture.diskEncrypted !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("diskEncrypted")}
|
||||
@@ -601,7 +618,8 @@ export default function GeneralPage() {
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.diskEncrypted
|
||||
client.posture
|
||||
.diskEncrypted
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
@@ -609,7 +627,8 @@ export default function GeneralPage() {
|
||||
)}
|
||||
|
||||
{client.posture.firewallEnabled !== null &&
|
||||
client.posture.firewallEnabled !== undefined && (
|
||||
client.posture.firewallEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("firewallEnabled")}
|
||||
@@ -617,15 +636,18 @@ export default function GeneralPage() {
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.firewallEnabled
|
||||
client.posture
|
||||
.firewallEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.autoUpdatesEnabled !== null &&
|
||||
client.posture.autoUpdatesEnabled !== undefined && (
|
||||
{client.posture.autoUpdatesEnabled !==
|
||||
null &&
|
||||
client.posture.autoUpdatesEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("autoUpdatesEnabled")}
|
||||
@@ -633,7 +655,8 @@ export default function GeneralPage() {
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.autoUpdatesEnabled
|
||||
client.posture
|
||||
.autoUpdatesEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
@@ -641,7 +664,8 @@ export default function GeneralPage() {
|
||||
)}
|
||||
|
||||
{client.posture.tpmAvailable !== null &&
|
||||
client.posture.tpmAvailable !== undefined && (
|
||||
client.posture.tpmAvailable !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("tpmAvailable")}
|
||||
@@ -649,18 +673,24 @@ export default function GeneralPage() {
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.tpmAvailable
|
||||
client.posture
|
||||
.tpmAvailable
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.windowsAntivirusEnabled !== null &&
|
||||
client.posture.windowsAntivirusEnabled !== undefined && (
|
||||
{client.posture.windowsAntivirusEnabled !==
|
||||
null &&
|
||||
client.posture
|
||||
.windowsAntivirusEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("windowsAntivirusEnabled")}
|
||||
{t(
|
||||
"windowsAntivirusEnabled"
|
||||
)}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
@@ -674,7 +704,8 @@ export default function GeneralPage() {
|
||||
)}
|
||||
|
||||
{client.posture.macosSipEnabled !== null &&
|
||||
client.posture.macosSipEnabled !== undefined && (
|
||||
client.posture.macosSipEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("macosSipEnabled")}
|
||||
@@ -682,19 +713,24 @@ export default function GeneralPage() {
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture.macosSipEnabled
|
||||
client.posture
|
||||
.macosSipEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.macosGatekeeperEnabled !== null &&
|
||||
client.posture.macosGatekeeperEnabled !==
|
||||
{client.posture.macosGatekeeperEnabled !==
|
||||
null &&
|
||||
client.posture
|
||||
.macosGatekeeperEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("macosGatekeeperEnabled")}
|
||||
{t(
|
||||
"macosGatekeeperEnabled"
|
||||
)}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
@@ -707,12 +743,16 @@ export default function GeneralPage() {
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.macosFirewallStealthMode !== null &&
|
||||
client.posture.macosFirewallStealthMode !==
|
||||
{client.posture.macosFirewallStealthMode !==
|
||||
null &&
|
||||
client.posture
|
||||
.macosFirewallStealthMode !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("macosFirewallStealthMode")}
|
||||
{t(
|
||||
"macosFirewallStealthMode"
|
||||
)}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
@@ -725,7 +765,8 @@ export default function GeneralPage() {
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.linuxAppArmorEnabled !== null &&
|
||||
{client.posture.linuxAppArmorEnabled !==
|
||||
null &&
|
||||
client.posture.linuxAppArmorEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
@@ -743,7 +784,8 @@ export default function GeneralPage() {
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.linuxSELinuxEnabled !== null &&
|
||||
{client.posture.linuxSELinuxEnabled !==
|
||||
null &&
|
||||
client.posture.linuxSELinuxEnabled !==
|
||||
undefined && (
|
||||
<InfoSection>
|
||||
|
||||
@@ -20,11 +20,6 @@ export interface AuthPageProps {
|
||||
export default async function AuthPage(props: AuthPageProps) {
|
||||
const orgId = (await props.params).orgId;
|
||||
|
||||
// custom auth branding is only available in enterprise and saas
|
||||
if (build === "oss") {
|
||||
redirect(`/${orgId}/settings/general/`);
|
||||
}
|
||||
|
||||
let subscriptionStatus: GetOrgTierResponse | null = null;
|
||||
try {
|
||||
const subRes = await getCachedSubscription(orgId);
|
||||
|
||||
@@ -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(`/`);
|
||||
@@ -55,14 +57,17 @@ export default async function GeneralSettingsPage({
|
||||
{
|
||||
title: t("security"),
|
||||
href: `/{orgId}/settings/general/security`
|
||||
}
|
||||
},
|
||||
// PaidFeaturesAlert
|
||||
...(!env.flags.disableEnterpriseFeatures
|
||||
? [
|
||||
{
|
||||
title: t("authPage"),
|
||||
href: `/{orgId}/settings/general/auth-page`
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
if (build !== "oss") {
|
||||
navItems.push({
|
||||
title: t("authPage"),
|
||||
href: `/{orgId}/settings/general/auth-page`
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -3,12 +3,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useActionState,
|
||||
type ComponentRef
|
||||
} from "react";
|
||||
import { useState, useRef, useActionState, type ComponentRef } from "react";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -107,10 +102,13 @@ type SectionFormProps = {
|
||||
|
||||
export default function SecurityPage() {
|
||||
const { org } = useOrgContext();
|
||||
const { env } = useEnvContext();
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<LogRetentionSectionForm org={org.org} />
|
||||
{build !== "oss" && <SecuritySettingsSectionForm org={org.org} />}
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<SecuritySettingsSectionForm org={org.org} />
|
||||
)}
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -140,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();
|
||||
@@ -243,7 +242,7 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
|
||||
)}
|
||||
/>
|
||||
|
||||
{build !== "oss" && (
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<>
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
@@ -740,7 +739,7 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
|
||||
type="submit"
|
||||
form="security-settings-section-form"
|
||||
loading={loadingSave}
|
||||
disabled={loadingSave}
|
||||
disabled={loadingSave || !isPaidUser}
|
||||
>
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import axios from "axios";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const router = useRouter();
|
||||
@@ -209,7 +210,8 @@ export default function GeneralPage() {
|
||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||
if (
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked())
|
||||
(build == "enterprise" && !isUnlocked()) ||
|
||||
build === "oss"
|
||||
) {
|
||||
console.log(
|
||||
"Access denied: subscription inactive or license locked"
|
||||
@@ -611,21 +613,7 @@ export default function GeneralPage() {
|
||||
description={t("accessLogsDescription")}
|
||||
/>
|
||||
|
||||
{build == "saas" && !subscription?.subscribed ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("subscriptionRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{build == "enterprise" && !isUnlocked() ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("licenseRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<LogDataTable
|
||||
columns={columns}
|
||||
@@ -656,7 +644,8 @@ export default function GeneralPage() {
|
||||
renderExpandedRow={renderExpandedRow}
|
||||
disabled={
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked())
|
||||
(build == "enterprise" && !isUnlocked()) ||
|
||||
build === "oss"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
@@ -92,6 +93,9 @@ export default function GeneralPage() {
|
||||
|
||||
// Trigger search with default values on component mount
|
||||
useEffect(() => {
|
||||
if (build === "oss") {
|
||||
return;
|
||||
}
|
||||
const defaultRange = getDefaultDateRange();
|
||||
queryDateTime(
|
||||
defaultRange.startDate,
|
||||
@@ -461,21 +465,7 @@ export default function GeneralPage() {
|
||||
description={t("actionLogsDescription")}
|
||||
/>
|
||||
|
||||
{build == "saas" && !subscription?.subscribed ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("subscriptionRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{build == "enterprise" && !isUnlocked() ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("licenseRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<LogDataTable
|
||||
columns={columns}
|
||||
@@ -508,7 +498,8 @@ export default function GeneralPage() {
|
||||
renderExpandedRow={renderExpandedRow}
|
||||
disabled={
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked())
|
||||
(build == "enterprise" && !isUnlocked()) ||
|
||||
build === "oss"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -16,6 +16,7 @@ import Link from "next/link";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const router = useRouter();
|
||||
@@ -110,6 +111,9 @@ export default function GeneralPage() {
|
||||
|
||||
// Trigger search with default values on component mount
|
||||
useEffect(() => {
|
||||
if (build === "oss") {
|
||||
return;
|
||||
}
|
||||
const defaultRange = getDefaultDateRange();
|
||||
queryDateTime(
|
||||
defaultRange.startDate,
|
||||
|
||||
@@ -44,6 +44,7 @@ import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import SetResourcePasswordForm from "components/SetResourcePasswordForm";
|
||||
@@ -164,7 +165,7 @@ export default function ResourceAuthenticationPage() {
|
||||
|
||||
const allIdps = useMemo(() => {
|
||||
if (build === "saas") {
|
||||
if (isPaidUser) {
|
||||
if (isPaidUser(tierMatrix.orgOidc)) {
|
||||
return orgIdps.map((idp) => ({
|
||||
id: idp.idpId,
|
||||
text: idp.name
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
|
||||
type MaintenanceSectionFormProps = {
|
||||
resource: GetResourceResponse;
|
||||
@@ -78,6 +79,7 @@ function MaintenanceSectionForm({
|
||||
const api = createApiClient({ env });
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const MaintenanceFormSchema = z.object({
|
||||
maintenanceModeEnabled: z.boolean().optional(),
|
||||
@@ -161,7 +163,9 @@ function MaintenanceSectionForm({
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
return (
|
||||
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
|
||||
);
|
||||
};
|
||||
|
||||
if (!resource.http) {
|
||||
@@ -187,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>
|
||||
@@ -413,7 +418,7 @@ function MaintenanceSectionForm({
|
||||
<Button
|
||||
type="submit"
|
||||
loading={maintenanceSaveLoading}
|
||||
disabled={maintenanceSaveLoading}
|
||||
disabled={maintenanceSaveLoading || !isPaidUser}
|
||||
form="maintenance-settings-form"
|
||||
>
|
||||
{t("saveSettings")}
|
||||
@@ -739,7 +744,7 @@ export default function GeneralForm() {
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
|
||||
{build !== "oss" && (
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<MaintenanceSectionForm
|
||||
resource={resource}
|
||||
updateResource={updateResource}
|
||||
|
||||
@@ -72,7 +72,9 @@ export default function CredentialsPage() {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
return (
|
||||
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
|
||||
);
|
||||
};
|
||||
|
||||
// Fetch site defaults for wireguard sites to show in obfuscated config
|
||||
@@ -269,7 +271,7 @@ export default function CredentialsPage() {
|
||||
</Alert>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
{build !== "oss" && (
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -383,7 +385,7 @@ export default function CredentialsPage() {
|
||||
</>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
{build === "enterprise" && (
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
|
||||
@@ -7,22 +7,35 @@ import { cache } from "react";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Props = {
|
||||
searchParams: Promise<{ code?: string }>;
|
||||
searchParams: Promise<{ code?: string; user?: string }>;
|
||||
};
|
||||
|
||||
function deviceRedirectSearchParams(params: {
|
||||
code?: string;
|
||||
user?: string;
|
||||
}): string {
|
||||
const search = new URLSearchParams();
|
||||
if (params.code) search.set("code", params.code);
|
||||
if (params.user) search.set("user", params.user);
|
||||
const q = search.toString();
|
||||
return q ? `?${q}` : "";
|
||||
}
|
||||
|
||||
export default async function DeviceLoginPage({ searchParams }: Props) {
|
||||
const user = await verifySession({ forceLogin: true });
|
||||
|
||||
const params = await searchParams;
|
||||
const code = params.code || "";
|
||||
const defaultUser = params.user;
|
||||
|
||||
if (!user) {
|
||||
const redirectDestination = code
|
||||
? `/auth/login/device?code=${encodeURIComponent(code)}`
|
||||
: "/auth/login/device";
|
||||
redirect(
|
||||
`/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectDestination)}`
|
||||
);
|
||||
const redirectDestination = `/auth/login/device${deviceRedirectSearchParams({ code, user: params.user })}`;
|
||||
const loginUrl = new URL("/auth/login", "http://x");
|
||||
loginUrl.searchParams.set("forceLogin", "true");
|
||||
loginUrl.searchParams.set("redirect", redirectDestination);
|
||||
if (defaultUser) loginUrl.searchParams.set("user", defaultUser);
|
||||
console.log("loginUrl", loginUrl.pathname + loginUrl.search);
|
||||
redirect(loginUrl.pathname + loginUrl.search);
|
||||
}
|
||||
|
||||
const userName = user
|
||||
@@ -37,6 +50,7 @@ export default async function DeviceLoginPage({ searchParams }: Props) {
|
||||
userEmail={user?.email || ""}
|
||||
userName={userName}
|
||||
initialCode={code}
|
||||
userQueryParam={defaultUser}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ export default async function Page(props: {
|
||||
searchParams.redirect = redirectUrl;
|
||||
}
|
||||
|
||||
const defaultUser = searchParams.user as string | undefined;
|
||||
|
||||
// Only use SmartLoginForm if NOT (OSS build OR org-only IdP enabled)
|
||||
const useSmartLogin =
|
||||
build === "saas" || (build === "enterprise" && env.flags.useOrgOnlyIdp);
|
||||
@@ -151,6 +153,7 @@ export default async function Page(props: {
|
||||
<SmartLoginForm
|
||||
redirect={redirectUrl}
|
||||
forceLogin={forceLogin}
|
||||
defaultUser={defaultUser}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -165,6 +168,7 @@ export default async function Page(props: {
|
||||
(build === "saas" || env.flags.useOrgOnlyIdp)
|
||||
}
|
||||
searchParams={searchParams}
|
||||
defaultUser={defaultUser}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -121,7 +121,10 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
|
||||
href: "/{orgId}/settings/access/roles",
|
||||
icon: <Users className="size-4 flex-none" />
|
||||
},
|
||||
...(build === "saas" || env?.flags.useOrgOnlyIdp
|
||||
// PaidFeaturesAlert
|
||||
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
|
||||
build === "saas" ||
|
||||
env?.flags.useOrgOnlyIdp
|
||||
? [
|
||||
{
|
||||
title: "sidebarIdentityProviders",
|
||||
@@ -130,7 +133,7 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(build !== "oss"
|
||||
...(!env?.flags.disableEnterpriseFeatures
|
||||
? [
|
||||
{
|
||||
title: "sidebarApprovals",
|
||||
@@ -155,7 +158,7 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
|
||||
href: "/{orgId}/settings/logs/request",
|
||||
icon: <SquareMousePointer className="size-4 flex-none" />
|
||||
},
|
||||
...(build != "oss"
|
||||
...(!env?.flags.disableEnterpriseFeatures
|
||||
? [
|
||||
{
|
||||
title: "sidebarLogsAccess",
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
import { Separator } from "./ui/separator";
|
||||
import { InfoPopup } from "./ui/info-popup";
|
||||
import { ApprovalsEmptyState } from "./ApprovalsEmptyState";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export type ApprovalFeedProps = {
|
||||
orgId: string;
|
||||
@@ -50,9 +52,12 @@ export function ApprovalFeed({
|
||||
Object.fromEntries(searchParams.entries())
|
||||
);
|
||||
|
||||
const { data, isFetching, refetch } = useQuery(
|
||||
approvalQueries.listApprovals(orgId, filters)
|
||||
);
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const { data, isFetching, refetch } = useQuery({
|
||||
...approvalQueries.listApprovals(orgId, filters),
|
||||
enabled: isPaidUser(tierMatrix.deviceApprovals)
|
||||
});
|
||||
|
||||
const approvals = data?.approvals ?? [];
|
||||
|
||||
@@ -209,19 +214,19 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
|
||||
|
||||
{approval.type === "user_device" && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{approval.deviceName ? (
|
||||
<>
|
||||
{t("requestingNewDeviceApproval")}:{" "}
|
||||
{approval.niceId ? (
|
||||
<Link
|
||||
href={`/${orgId}/settings/clients/user/${approval.niceId}/general`}
|
||||
className="text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
{approval.deviceName}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{approval.deviceName}</span>
|
||||
)}
|
||||
{approval.deviceName ? (
|
||||
<>
|
||||
{t("requestingNewDeviceApproval")}:{" "}
|
||||
{approval.niceId ? (
|
||||
<Link
|
||||
href={`/${orgId}/settings/clients/user/${approval.niceId}/general`}
|
||||
className="text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
{approval.deviceName}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{approval.deviceName}</span>
|
||||
)}
|
||||
{approval.fingerprint && (
|
||||
<InfoPopup>
|
||||
<div className="space-y-1 text-sm">
|
||||
@@ -229,7 +234,10 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
|
||||
{t("deviceInformation")}
|
||||
</div>
|
||||
<div className="text-muted-foreground whitespace-pre-line">
|
||||
{formatFingerprintInfo(approval.fingerprint, t)}
|
||||
{formatFingerprintInfo(
|
||||
approval.fingerprint,
|
||||
t
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</InfoPopup>
|
||||
|
||||
@@ -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
|
||||
@@ -160,8 +161,9 @@ export default function CreateRoleForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{build !== "oss" && (
|
||||
<div>
|
||||
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<>
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<FormField
|
||||
@@ -208,7 +210,7 @@ export default function CreateRoleForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -29,6 +29,7 @@ type DashboardLoginFormProps = {
|
||||
searchParams?: {
|
||||
[key: string]: string | string[] | undefined;
|
||||
};
|
||||
defaultUser?: string;
|
||||
};
|
||||
|
||||
export default function DashboardLoginForm({
|
||||
@@ -36,7 +37,8 @@ export default function DashboardLoginForm({
|
||||
idps,
|
||||
forceLogin,
|
||||
showOrgLogin,
|
||||
searchParams
|
||||
searchParams,
|
||||
defaultUser
|
||||
}: DashboardLoginFormProps) {
|
||||
const router = useRouter();
|
||||
const { env } = useEnvContext();
|
||||
@@ -75,6 +77,7 @@ export default function DashboardLoginForm({
|
||||
redirect={redirect}
|
||||
idps={idps}
|
||||
forceLogin={forceLogin}
|
||||
defaultEmail={defaultUser}
|
||||
onLogin={(redirectUrl) => {
|
||||
if (redirectUrl) {
|
||||
const safe = cleanRedirect(redirectUrl);
|
||||
|
||||
@@ -55,12 +55,14 @@ type DeviceLoginFormProps = {
|
||||
userEmail: string;
|
||||
userName?: string;
|
||||
initialCode?: string;
|
||||
userQueryParam?: string;
|
||||
};
|
||||
|
||||
export default function DeviceLoginForm({
|
||||
userEmail,
|
||||
userName,
|
||||
initialCode = ""
|
||||
initialCode = "",
|
||||
userQueryParam
|
||||
}: DeviceLoginFormProps) {
|
||||
const router = useRouter();
|
||||
const { env } = useEnvContext();
|
||||
@@ -219,9 +221,12 @@ export default function DeviceLoginForm({
|
||||
const currentSearch =
|
||||
typeof window !== "undefined" ? window.location.search : "";
|
||||
const redirectTarget = `/auth/login/device${currentSearch || ""}`;
|
||||
router.push(
|
||||
`/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectTarget)}`
|
||||
);
|
||||
const loginUrl = new URL("/auth/login", "http://x");
|
||||
loginUrl.searchParams.set("forceLogin", "true");
|
||||
loginUrl.searchParams.set("redirect", redirectTarget);
|
||||
if (userQueryParam)
|
||||
loginUrl.searchParams.set("user", userQueryParam);
|
||||
router.push(loginUrl.pathname + loginUrl.search);
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +169,9 @@ export default function EditRoleForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{build !== "oss" && (
|
||||
<div>
|
||||
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<>
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<FormField
|
||||
@@ -216,7 +218,7 @@ export default function EditRoleForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -54,6 +54,7 @@ type LoginFormProps = {
|
||||
idps?: LoginFormIDP[];
|
||||
orgId?: string;
|
||||
forceLogin?: boolean;
|
||||
defaultEmail?: string;
|
||||
};
|
||||
|
||||
export default function LoginForm({
|
||||
@@ -61,7 +62,8 @@ export default function LoginForm({
|
||||
onLogin,
|
||||
idps,
|
||||
orgId,
|
||||
forceLogin
|
||||
forceLogin,
|
||||
defaultEmail
|
||||
}: LoginFormProps) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -116,7 +118,7 @@ export default function LoginForm({
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
email: defaultEmail ?? "",
|
||||
password: ""
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,28 +1,74 @@
|
||||
"use client";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import { build } from "@server/build";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { ExternalLink, KeyRound, Sparkles } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
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";
|
||||
const bannerContentClassName = "py-3 px-4";
|
||||
const bannerRowClassName =
|
||||
"flex items-center gap-2.5 text-sm text-muted-foreground";
|
||||
|
||||
export function PaidFeaturesAlert() {
|
||||
const t = useTranslations();
|
||||
const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
if (env.flags.disableEnterpriseFeatures) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{build === "saas" && !hasSaasSubscription ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("subscriptionRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Card className={bannerClassName}>
|
||||
<CardContent className={bannerContentClassName}>
|
||||
<div className={bannerRowClassName}>
|
||||
<KeyRound className="size-4 shrink-0 text-primary" />
|
||||
<span>{t("subscriptionRequiredToUse")}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{build === "enterprise" && !hasEnterpriseLicense ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("licenseRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Card className={bannerClassName}>
|
||||
<CardContent className={bannerContentClassName}>
|
||||
<div className={bannerRowClassName}>
|
||||
<KeyRound className="size-4 shrink-0 text-primary" />
|
||||
<span>{t("licenseRequiredToUse")}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{build === "oss" && !hasEnterpriseLicense ? (
|
||||
<Card className="mb-6 border-purple-500/30 bg-linear-to-br from-purple-500/10 via-background to-background overflow-hidden">
|
||||
<CardContent className={bannerContentClassName}>
|
||||
<div className={bannerRowClassName}>
|
||||
<KeyRound className="size-4 shrink-0 text-purple-500" />
|
||||
<span>
|
||||
{t.rich("ossEnterpriseEditionRequired", {
|
||||
enterpriseEditionLink: (chunks) => (
|
||||
<Link
|
||||
href="https://docs.pangolin.net/self-host/enterprise-edition"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-medium text-purple-600 underline"
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
@@ -42,6 +42,7 @@ const isValidEmail = (str: string): boolean => {
|
||||
type SmartLoginFormProps = {
|
||||
redirect?: string;
|
||||
forceLogin?: boolean;
|
||||
defaultUser?: string;
|
||||
};
|
||||
|
||||
type ViewState =
|
||||
@@ -59,7 +60,8 @@ type ViewState =
|
||||
|
||||
export default function SmartLoginForm({
|
||||
redirect,
|
||||
forceLogin
|
||||
forceLogin,
|
||||
defaultUser
|
||||
}: SmartLoginFormProps) {
|
||||
const router = useRouter();
|
||||
const { lookup, loading, error } = useUserLookup();
|
||||
@@ -72,10 +74,18 @@ export default function SmartLoginForm({
|
||||
const form = useForm<z.infer<typeof identifierSchema>>({
|
||||
resolver: zodResolver(identifierSchema),
|
||||
defaultValues: {
|
||||
identifier: ""
|
||||
identifier: defaultUser ?? ""
|
||||
}
|
||||
});
|
||||
|
||||
const hasAutoLookedUp = useRef(false);
|
||||
useEffect(() => {
|
||||
if (defaultUser?.trim() && !hasAutoLookedUp.current) {
|
||||
hasAutoLookedUp.current = true;
|
||||
void handleLookup({ identifier: defaultUser.trim() });
|
||||
}
|
||||
}, [defaultUser]);
|
||||
|
||||
const handleLookup = async (values: z.infer<typeof identifierSchema>) => {
|
||||
const identifier = values.identifier.trim();
|
||||
const isEmail = isValidEmail(identifier);
|
||||
|
||||
@@ -190,7 +190,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
const approvalsRes = await api.get<{
|
||||
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
||||
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
||||
|
||||
|
||||
const approval = approvalsRes.data.data.approvals[0];
|
||||
|
||||
if (!approval) {
|
||||
@@ -232,7 +232,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
const approvalsRes = await api.get<{
|
||||
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
||||
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
||||
|
||||
|
||||
const approval = approvalsRes.data.data.approvals[0];
|
||||
|
||||
if (!approval) {
|
||||
@@ -548,7 +548,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{clientRow.approvalState === "pending" && build !== "oss" && (
|
||||
{clientRow.approvalState === "pending" && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => approveDevice(clientRow)}
|
||||
@@ -652,17 +652,10 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
}
|
||||
];
|
||||
|
||||
if (build === "oss") {
|
||||
return allOptions.filter((option) => option.value !== "pending" && option.value !== "denied");
|
||||
}
|
||||
|
||||
return allOptions;
|
||||
}, [t]);
|
||||
|
||||
const statusFilterDefaultValues = useMemo(() => {
|
||||
if (build === "oss") {
|
||||
return ["active"];
|
||||
}
|
||||
return ["active", "pending"];
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -43,11 +43,11 @@ export function OlmInstallCommands({
|
||||
All: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash`
|
||||
command: `curl -fsSL https://static.pangolin.net/get-cli.sh | bash`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
command: `sudo pangolin up --id ${id} --secret ${secret} --endpoint ${endpoint} --attach`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
import { createContext } from "react";
|
||||
|
||||
type SubscriptionStatusContextType = {
|
||||
subscriptionStatus: GetOrgSubscriptionResponse | null;
|
||||
updateSubscriptionStatus: (updatedSite: GetOrgSubscriptionResponse) => void;
|
||||
isActive: () => boolean;
|
||||
getTier: () => { tier: string | null; active: boolean };
|
||||
getTier: () => { tier: Tier | null; active: boolean };
|
||||
isSubscribed: () => boolean;
|
||||
subscribed: boolean;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { build } from "@server/build";
|
||||
import { useLicenseStatusContext } from "./useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "./useSubscriptionStatusContext";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export function usePaidStatus() {
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
@@ -8,14 +9,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: Tier[]): 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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -35,6 +35,7 @@ export type Env = {
|
||||
usePangolinDns: boolean;
|
||||
disableProductHelpBanners: boolean;
|
||||
useOrgOnlyIdp: boolean;
|
||||
disableEnterpriseFeatures: boolean;
|
||||
};
|
||||
branding: {
|
||||
appName?: string;
|
||||
|
||||
@@ -2,29 +2,6 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
// If build is OSS, block access to private routes
|
||||
if (build === "oss") {
|
||||
const pathname = request.nextUrl.pathname;
|
||||
|
||||
// Define private route patterns that should be blocked in OSS build
|
||||
const privateRoutes = [
|
||||
"/settings/billing",
|
||||
"/settings/remote-exit-nodes",
|
||||
"/settings/idp",
|
||||
"/auth/org"
|
||||
];
|
||||
|
||||
// Check if current path matches any private route pattern
|
||||
const isPrivateRoute = privateRoutes.some((route) =>
|
||||
pathname.includes(route)
|
||||
);
|
||||
|
||||
if (isPrivateRoute) {
|
||||
// Return 404 to make it seem like the route doesn't exist
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import SubscriptionStatusContext from "@app/contexts/subscriptionStatusContext";
|
||||
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
|
||||
import { useState } from "react";
|
||||
import { build } from "@server/build";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
interface ProviderProps {
|
||||
children: React.ReactNode;
|
||||
@@ -31,17 +32,10 @@ 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 = (): {
|
||||
tier: Tier | null;
|
||||
active: boolean;
|
||||
} => {
|
||||
if (subscriptionStatus?.subscriptions) {
|
||||
// Iterate through all subscriptions
|
||||
for (const { subscription } of subscriptionStatus.subscriptions) {
|
||||
@@ -65,9 +59,6 @@ export function SubscriptionStatusProvider({
|
||||
};
|
||||
|
||||
const isSubscribed = () => {
|
||||
if (build === "enterprise") {
|
||||
return true;
|
||||
}
|
||||
const { tier, active } = getTier();
|
||||
return (
|
||||
(tier == "tier1" || tier == "tier2" || tier == "tier3") &&
|
||||
@@ -82,7 +73,6 @@ export function SubscriptionStatusProvider({
|
||||
value={{
|
||||
subscriptionStatus: subscriptionStatusState,
|
||||
updateSubscriptionStatus,
|
||||
isActive,
|
||||
getTier,
|
||||
isSubscribed,
|
||||
subscribed
|
||||
|
||||
Reference in New Issue
Block a user