mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
Compare commits
13 Commits
a095dddd01
...
new-pricin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94ac3ec76e | ||
|
|
af7263a0b1 | ||
|
|
035396f95c | ||
|
|
f318f6304b | ||
|
|
9d0ff472e5 | ||
|
|
d27482e812 | ||
|
|
69c2212ea0 | ||
|
|
10be9bcd56 | ||
|
|
f531def0d2 | ||
|
|
ed40eae655 | ||
|
|
ba5ae6ed04 | ||
|
|
0a6301697e | ||
|
|
13b4fc6725 |
@@ -1,6 +1,3 @@
|
||||
import Stripe from "stripe";
|
||||
import { usageService } from "./usageService";
|
||||
|
||||
export enum FeatureId {
|
||||
USERS = "users",
|
||||
SITES = "sites",
|
||||
@@ -135,25 +132,3 @@ export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getLineItems(
|
||||
featurePriceSet: FeaturePriceSet,
|
||||
orgId: string,
|
||||
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
|
||||
const users = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||
|
||||
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
|
||||
let quantity: number | undefined;
|
||||
|
||||
if (featureId === FeatureId.USERS) {
|
||||
quantity = users?.instantaneousValue || 1;
|
||||
} else if (featureId === FeatureId.TIER1) {
|
||||
quantity = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
price: priceId,
|
||||
quantity: quantity
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
25
server/lib/billing/getLineItems.ts
Normal file
25
server/lib/billing/getLineItems.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Stripe from "stripe";
|
||||
import { FeatureId, FeaturePriceSet } from "./features";
|
||||
import { usageService } from "./usageService";
|
||||
|
||||
export async function getLineItems(
|
||||
featurePriceSet: FeaturePriceSet,
|
||||
orgId: string,
|
||||
): Promise<Stripe.Checkout.SessionCreateParams.LineItem[]> {
|
||||
const users = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||
|
||||
return Object.entries(featurePriceSet).map(([featureId, priceId]) => {
|
||||
let quantity: number | undefined;
|
||||
|
||||
if (featureId === FeatureId.USERS) {
|
||||
quantity = users?.instantaneousValue || 1;
|
||||
} else if (featureId === FeatureId.TIER1) {
|
||||
quantity = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
price: priceId,
|
||||
quantity: quantity
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Tier } from "@server/types/Tiers";
|
||||
|
||||
export enum TierFeature {
|
||||
OrgOidc = "orgOidc",
|
||||
CustomAuthenticationDomain = "customAuthenticationDomain",
|
||||
LoginPageDomain = "loginPageDomain",
|
||||
DeviceApprovals = "deviceApprovals",
|
||||
LoginPageBranding = "loginPageBranding",
|
||||
LogExport = "logExport",
|
||||
@@ -13,17 +13,13 @@ export enum TierFeature {
|
||||
DevicePosture = "devicePosture",
|
||||
TwoFactorEnforcement = "twoFactorEnforcement",
|
||||
SessionDurationPolicies = "sessionDurationPolicies",
|
||||
PasswordExpirationPolicies = "passwordExpirationPolicies"
|
||||
PasswordExpirationPolicies = "passwordExpirationPolicies",
|
||||
AutoProvisioning = "autoProvisioning"
|
||||
}
|
||||
|
||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.CustomAuthenticationDomain]: [
|
||||
"tier1",
|
||||
"tier2",
|
||||
"tier3",
|
||||
"enterprise"
|
||||
],
|
||||
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
|
||||
[TierFeature.LoginPageBranding]: ["tier1", "tier3", "enterprise"],
|
||||
[TierFeature.LogExport]: ["tier3", "enterprise"],
|
||||
@@ -32,7 +28,23 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||
[TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.MaintencePage]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"],
|
||||
[TierFeature.TwoFactorEnforcement]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.SessionDurationPolicies]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||
[TierFeature.PasswordExpirationPolicies]: ["tier1", "tier2", "tier3", "enterprise"]
|
||||
[TierFeature.TwoFactorEnforcement]: [
|
||||
"tier1",
|
||||
"tier2",
|
||||
"tier3",
|
||||
"enterprise"
|
||||
],
|
||||
[TierFeature.SessionDurationPolicies]: [
|
||||
"tier1",
|
||||
"tier2",
|
||||
"tier3",
|
||||
"enterprise"
|
||||
],
|
||||
[TierFeature.PasswordExpirationPolicies]: [
|
||||
"tier1",
|
||||
"tier2",
|
||||
"tier3",
|
||||
"enterprise"
|
||||
],
|
||||
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"]
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { build } from "@server/build";
|
||||
import { s3Client } from "@server/lib/s3";
|
||||
import cache from "@server/lib/cache";
|
||||
import privateConfig from "@server/private/lib/config";
|
||||
|
||||
interface StripeEvent {
|
||||
identifier?: string;
|
||||
@@ -48,39 +47,31 @@ export class UsageService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bucketName = process.env.S3_BUCKET || undefined;
|
||||
// this.bucketName = process.env.S3_BUCKET || undefined;
|
||||
|
||||
if (
|
||||
// Only set up event uploading if usage reporting is enabled and bucket name is configured
|
||||
privateConfig.getRawPrivateConfig().flags.usage_reporting &&
|
||||
this.bucketName
|
||||
) {
|
||||
// Periodically check and upload events
|
||||
setInterval(() => {
|
||||
this.checkAndUploadEvents().catch((err) => {
|
||||
logger.error("Error in periodic event upload:", err);
|
||||
});
|
||||
}, 30000); // every 30 seconds
|
||||
// // Periodically check and upload events
|
||||
// setInterval(() => {
|
||||
// this.checkAndUploadEvents().catch((err) => {
|
||||
// logger.error("Error in periodic event upload:", err);
|
||||
// });
|
||||
// }, 30000); // every 30 seconds
|
||||
|
||||
// Handle graceful shutdown on SIGTERM
|
||||
process.on("SIGTERM", async () => {
|
||||
logger.info(
|
||||
"SIGTERM received, uploading events before shutdown..."
|
||||
);
|
||||
await this.forceUpload();
|
||||
logger.info("Events uploaded, proceeding with shutdown");
|
||||
});
|
||||
// // Handle graceful shutdown on SIGTERM
|
||||
// process.on("SIGTERM", async () => {
|
||||
// logger.info(
|
||||
// "SIGTERM received, uploading events before shutdown..."
|
||||
// );
|
||||
// await this.forceUpload();
|
||||
// logger.info("Events uploaded, proceeding with shutdown");
|
||||
// });
|
||||
|
||||
// Handle SIGINT as well (Ctrl+C)
|
||||
process.on("SIGINT", async () => {
|
||||
logger.info(
|
||||
"SIGINT received, uploading events before shutdown..."
|
||||
);
|
||||
await this.forceUpload();
|
||||
logger.info("Events uploaded, proceeding with shutdown");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
// // Handle SIGINT as well (Ctrl+C)
|
||||
// process.on("SIGINT", async () => {
|
||||
// logger.info("SIGINT received, uploading events before shutdown...");
|
||||
// await this.forceUpload();
|
||||
// logger.info("Events uploaded, proceeding with shutdown");
|
||||
// process.exit(0);
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,9 +130,9 @@ export class UsageService {
|
||||
}
|
||||
|
||||
// Log event for Stripe
|
||||
if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
|
||||
await this.logStripeEvent(featureId, value, customerId);
|
||||
}
|
||||
// if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
|
||||
// await this.logStripeEvent(featureId, value, customerId);
|
||||
// }
|
||||
|
||||
return usage || null;
|
||||
} catch (error: any) {
|
||||
@@ -282,9 +273,9 @@ export class UsageService {
|
||||
}
|
||||
});
|
||||
|
||||
if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
|
||||
await this.logStripeEvent(featureId, value || 0, customerId);
|
||||
}
|
||||
// if (privateConfig.getRawPrivateConfig().flags.usage_reporting) {
|
||||
// await this.logStripeEvent(featureId, value || 0, customerId);
|
||||
// }
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to update count usage for ${orgId}/${featureId}:`,
|
||||
|
||||
@@ -33,7 +33,6 @@ import { hashPassword } from "@server/auth/password";
|
||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "../billing/tierMatrix";
|
||||
import { t } from "@faker-js/faker/dist/airline-DF6RqYmq";
|
||||
|
||||
export type ProxyResourcesResults = {
|
||||
proxyResource: Resource;
|
||||
|
||||
@@ -29,3 +29,4 @@ export * from "./verifyUserIsOrgOwner";
|
||||
export * from "./verifySiteResourceAccess";
|
||||
export * from "./logActionAudit";
|
||||
export * from "./verifyOlmAccess";
|
||||
export * from "./verifyLimits";
|
||||
|
||||
@@ -4,7 +4,6 @@ import { apiKeyOrg } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function verifyApiKeyOrgAccess(
|
||||
req: Request,
|
||||
|
||||
47
server/middlewares/verifyLimits.ts
Normal file
47
server/middlewares/verifyLimits.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db, orgs } from "@server/db";
|
||||
import { userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export async function verifyLimits(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (build != "saas") {
|
||||
return next();
|
||||
}
|
||||
|
||||
const orgId = req.userOrgId || req.apiKeyOrg?.orgId || req.params.orgId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(); // its fine if we silently fail here because this is not critical to operation or security and its better user experience if we dont fail
|
||||
}
|
||||
|
||||
try {
|
||||
const reject = await usageService.checkLimitSet(orgId);
|
||||
|
||||
if (reject) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.PAYMENT_REQUIRED,
|
||||
"Organization has exceeded its usage limits. Please upgrade your plan or contact support."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (e) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Error checking limits"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,6 @@ export const privateConfigSchema = z.object({
|
||||
enable_redis: z.boolean().optional().default(false),
|
||||
use_pangolin_dns: z.boolean().optional().default(false),
|
||||
use_org_only_idp: z.boolean().optional().default(false),
|
||||
usage_reporting: z.boolean().optional().default(false)
|
||||
})
|
||||
.optional()
|
||||
.prefault({}),
|
||||
|
||||
@@ -45,7 +45,7 @@ export function verifyValidSubscription(tiers: Tier[]) {
|
||||
}
|
||||
|
||||
const { tier, active } = await getOrgTierData(orgId);
|
||||
const isTier = tiers.includes(tier || "");
|
||||
const isTier = tiers.includes(tier as Tier);
|
||||
if (!active) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
||||
@@ -25,10 +25,10 @@ import {
|
||||
getHomeLabFeaturePriceSet,
|
||||
getScaleFeaturePriceSet,
|
||||
getStarterFeaturePriceSet,
|
||||
getLineItems,
|
||||
FeatureId,
|
||||
type FeaturePriceSet
|
||||
} from "@server/lib/billing";
|
||||
import { getLineItems } from "@server/lib/billing/getLineItems";
|
||||
|
||||
const changeTierSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -151,8 +151,10 @@ export async function changeTier(
|
||||
// tier1 uses TIER1 product, tier2/tier3 use USERS product
|
||||
const currentTier = subscription.type;
|
||||
const switchingProducts =
|
||||
(currentTier === "tier1" && (tier === "tier2" || tier === "tier3")) ||
|
||||
((currentTier === "tier2" || currentTier === "tier3") && tier === "tier1");
|
||||
(currentTier === "tier1" &&
|
||||
(tier === "tier2" || tier === "tier3")) ||
|
||||
((currentTier === "tier2" || currentTier === "tier3") &&
|
||||
tier === "tier1");
|
||||
|
||||
let updatedSubscription;
|
||||
|
||||
|
||||
@@ -22,8 +22,12 @@ import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import stripe from "#private/lib/stripe";
|
||||
import { getHomeLabFeaturePriceSet, getLineItems, getScaleFeaturePriceSet, getStarterFeaturePriceSet } from "@server/lib/billing";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import {
|
||||
getHomeLabFeaturePriceSet,
|
||||
getScaleFeaturePriceSet,
|
||||
getStarterFeaturePriceSet
|
||||
} from "@server/lib/billing";
|
||||
import { getLineItems } from "@server/lib/billing/getLineItems";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const createCheckoutSessionSchema = z.strictObject({
|
||||
@@ -31,7 +35,7 @@ const createCheckoutSessionSchema = z.strictObject({
|
||||
});
|
||||
|
||||
const createCheckoutSessionBodySchema = z.strictObject({
|
||||
tier: z.enum(["tier1", "tier2", "tier3"]),
|
||||
tier: z.enum(["tier1", "tier2", "tier3"])
|
||||
});
|
||||
|
||||
export async function createCheckoutSession(
|
||||
@@ -90,12 +94,10 @@ export async function createCheckoutSession(
|
||||
} else if (tier === "tier3") {
|
||||
lineItems = await getLineItems(getScaleFeaturePriceSet(), orgId);
|
||||
} else {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid plan")
|
||||
);
|
||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid plan"));
|
||||
}
|
||||
|
||||
logger.debug(`Line items: ${JSON.stringify(lineItems)}`)
|
||||
logger.debug(`Line items: ${JSON.stringify(lineItems)}`);
|
||||
|
||||
const session = await stripe!.checkout.sessions.create({
|
||||
client_reference_id: orgId, // So we can look it up the org later on the webhook
|
||||
|
||||
@@ -90,7 +90,7 @@ export async function handleSubscriptionUpdated(
|
||||
const itemsToUpsert = fullSubscription.items.data.map((item) => {
|
||||
// Try to get featureId from price
|
||||
let featureId: string | null = getFeatureIdByPriceId(item.price.id) || null;
|
||||
|
||||
|
||||
// If no match, try to preserve existing featureId
|
||||
if (!featureId) {
|
||||
const existingItem = existingItems.find(
|
||||
|
||||
@@ -31,7 +31,8 @@ import {
|
||||
verifyUserHasAction,
|
||||
verifyUserIsServerAdmin,
|
||||
verifySiteAccess,
|
||||
verifyClientAccess
|
||||
verifyClientAccess,
|
||||
verifyLimits
|
||||
} from "@server/middlewares";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import {
|
||||
@@ -79,6 +80,7 @@ authenticated.put(
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.orgOidc),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createIdp),
|
||||
logActionAudit(ActionsEnum.createIdp),
|
||||
orgIdp.createOrgOidcIdp
|
||||
@@ -90,6 +92,7 @@ authenticated.post(
|
||||
verifyValidSubscription(tierMatrix.orgOidc),
|
||||
verifyOrgAccess,
|
||||
verifyIdpAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateIdp),
|
||||
logActionAudit(ActionsEnum.updateIdp),
|
||||
orgIdp.updateOrgOidcIdp
|
||||
@@ -138,6 +141,7 @@ authenticated.post(
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyCertificateAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.restartCertificate),
|
||||
logActionAudit(ActionsEnum.restartCertificate),
|
||||
certificates.restartCertificate
|
||||
@@ -237,6 +241,7 @@ authenticated.put(
|
||||
"/org/:orgId/remote-exit-node",
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createRemoteExitNode),
|
||||
logActionAudit(ActionsEnum.createRemoteExitNode),
|
||||
remoteExitNode.createRemoteExitNode
|
||||
@@ -280,8 +285,9 @@ authenticated.delete(
|
||||
authenticated.put(
|
||||
"/org/:orgId/login-page",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.customAuthenticationDomain),
|
||||
verifyValidSubscription(tierMatrix.loginPageDomain),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createLoginPage),
|
||||
logActionAudit(ActionsEnum.createLoginPage),
|
||||
loginPage.createLoginPage
|
||||
@@ -290,9 +296,10 @@ authenticated.put(
|
||||
authenticated.post(
|
||||
"/org/:orgId/login-page/:loginPageId",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.customAuthenticationDomain),
|
||||
verifyValidSubscription(tierMatrix.loginPageDomain),
|
||||
verifyOrgAccess,
|
||||
verifyLoginPageAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateLoginPage),
|
||||
logActionAudit(ActionsEnum.updateLoginPage),
|
||||
loginPage.updateLoginPage
|
||||
@@ -338,6 +345,7 @@ authenticated.put(
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.deviceApprovals),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateApprovals),
|
||||
logActionAudit(ActionsEnum.updateApprovals),
|
||||
approval.processPendingApproval
|
||||
@@ -358,6 +366,7 @@ authenticated.put(
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.loginPageBranding),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateLoginPage),
|
||||
logActionAudit(ActionsEnum.updateLoginPage),
|
||||
loginPage.upsertLoginPageBranding
|
||||
@@ -470,18 +479,20 @@ authenticated.get(
|
||||
|
||||
authenticated.post(
|
||||
"/re-key/:clientId/regenerate-client-secret",
|
||||
verifyClientAccess, // this is first to set the org id
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.rotateCredentials),
|
||||
verifyClientAccess, // this is first to set the org id
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||
reKey.reGenerateClientSecret
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/re-key/:siteId/regenerate-site-secret",
|
||||
verifySiteAccess, // this is first to set the org id
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.rotateCredentials),
|
||||
verifySiteAccess, // this is first to set the org id
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||
reKey.reGenerateSiteSecret
|
||||
);
|
||||
@@ -491,6 +502,7 @@ authenticated.put(
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.rotateCredentials),
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.reGenerateSecret),
|
||||
reKey.reGenerateExitNodeSecret
|
||||
);
|
||||
|
||||
@@ -19,21 +19,20 @@ import {
|
||||
verifyApiKeyHasAction,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyIdpAccess
|
||||
verifyApiKeyIdpAccess,
|
||||
verifyLimits
|
||||
} from "@server/middlewares";
|
||||
import {
|
||||
verifyValidSubscription,
|
||||
verifyValidLicense
|
||||
} from "#private/middlewares";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
|
||||
import {
|
||||
unauthenticated as ua,
|
||||
authenticated as a
|
||||
} from "@server/routers/integration";
|
||||
import { logActionAudit } from "#private/middlewares";
|
||||
import config from "#private/lib/config";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export const unauthenticated = ua;
|
||||
export const authenticated = a;
|
||||
@@ -57,7 +56,7 @@ authenticated.delete(
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/action",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.actionLogs),
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.exportLogs),
|
||||
logs.queryActionAuditLogs
|
||||
@@ -66,7 +65,7 @@ authenticated.get(
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/action/export",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.logExport),
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.exportLogs),
|
||||
logActionAudit(ActionsEnum.exportLogs),
|
||||
@@ -76,7 +75,7 @@ authenticated.get(
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/access",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.accessLogs),
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.exportLogs),
|
||||
logs.queryAccessAuditLogs
|
||||
@@ -85,7 +84,7 @@ authenticated.get(
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/access/export",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription,
|
||||
verifyValidSubscription(tierMatrix.logExport),
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.exportLogs),
|
||||
logActionAudit(ActionsEnum.exportLogs),
|
||||
@@ -95,7 +94,9 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/org/:orgId/idp/oidc",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.orgOidc),
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdp),
|
||||
logActionAudit(ActionsEnum.createIdp),
|
||||
orgIdp.createOrgOidcIdp
|
||||
@@ -104,8 +105,10 @@ authenticated.put(
|
||||
authenticated.post(
|
||||
"/org/:orgId/idp/:idpId/oidc",
|
||||
verifyValidLicense,
|
||||
verifyValidSubscription(tierMatrix.orgOidc),
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyIdpAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdp),
|
||||
logActionAudit(ActionsEnum.updateIdp),
|
||||
orgIdp.updateOrgOidcIdp
|
||||
|
||||
@@ -25,6 +25,8 @@ import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
|
||||
|
||||
@@ -100,12 +102,21 @@ export async function createOrgOidcIdp(
|
||||
emailPath,
|
||||
namePath,
|
||||
name,
|
||||
autoProvision,
|
||||
variant,
|
||||
roleMapping,
|
||||
tags
|
||||
} = parsedBody.data;
|
||||
|
||||
let { autoProvision } = parsedBody.data;
|
||||
|
||||
const subscribed = await isSubscribed(
|
||||
orgId,
|
||||
tierMatrix.deviceApprovals
|
||||
);
|
||||
if (!subscribed) {
|
||||
autoProvision = false;
|
||||
}
|
||||
|
||||
const key = config.getRawConfig().server.secret!;
|
||||
|
||||
const encryptedSecret = encrypt(clientSecret, key);
|
||||
|
||||
@@ -24,6 +24,8 @@ import { idp, idpOidcConfig } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import config from "@server/lib/config";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
@@ -106,11 +108,20 @@ export async function updateOrgOidcIdp(
|
||||
emailPath,
|
||||
namePath,
|
||||
name,
|
||||
autoProvision,
|
||||
roleMapping,
|
||||
tags
|
||||
} = parsedBody.data;
|
||||
|
||||
let { autoProvision } = parsedBody.data;
|
||||
|
||||
const subscribed = await isSubscribed(
|
||||
orgId,
|
||||
tierMatrix.deviceApprovals
|
||||
);
|
||||
if (!subscribed) {
|
||||
autoProvision = false;
|
||||
}
|
||||
|
||||
// Check if IDP exists and is of type OIDC
|
||||
const [existingIdp] = await db
|
||||
.select()
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
ResourceHeaderAuthExtendedCompatibility,
|
||||
ResourcePassword,
|
||||
ResourcePincode,
|
||||
ResourceRule,
|
||||
ResourceRule
|
||||
} from "@server/db";
|
||||
import config from "@server/lib/config";
|
||||
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
|
||||
@@ -40,6 +40,7 @@ import { logRequestAudit } from "./logRequestAudit";
|
||||
import cache from "@server/lib/cache";
|
||||
import { APP_VERSION } from "@server/lib/consts";
|
||||
import { isSubscribed } from "#private/lib/isSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const verifyResourceSessionSchema = z.object({
|
||||
sessions: z.record(z.string(), z.string()).optional(),
|
||||
@@ -796,7 +797,10 @@ async function notAllowed(
|
||||
) {
|
||||
let loginPage: LoginPage | null = null;
|
||||
if (orgId) {
|
||||
const subscribed = await isSubscribed(orgId);
|
||||
const subscribed = await isSubscribed(
|
||||
orgId,
|
||||
tierMatrix.loginPageDomain
|
||||
);
|
||||
if (subscribed) {
|
||||
loginPage = await getOrgLoginPage(orgId);
|
||||
}
|
||||
@@ -850,7 +854,7 @@ async function headerAuthChallenged(
|
||||
) {
|
||||
let loginPage: LoginPage | null = null;
|
||||
if (orgId) {
|
||||
const subscribed = await isSubscribed(orgId);
|
||||
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain);
|
||||
if (subscribed) {
|
||||
loginPage = await getOrgLoginPage(orgId);
|
||||
}
|
||||
@@ -1037,7 +1041,11 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
const MAX_RECURSION_DEPTH = 100;
|
||||
|
||||
// Recursive function to try different wildcard matches
|
||||
function matchSegments(patternIndex: number, pathIndex: number, depth: number = 0): boolean {
|
||||
function matchSegments(
|
||||
patternIndex: number,
|
||||
pathIndex: number,
|
||||
depth: number = 0
|
||||
): boolean {
|
||||
// Check recursion depth limit
|
||||
if (depth > MAX_RECURSION_DEPTH) {
|
||||
logger.warn(
|
||||
@@ -1123,7 +1131,11 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
logger.debug(
|
||||
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
|
||||
);
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
|
||||
return matchSegments(
|
||||
patternIndex + 1,
|
||||
pathIndex + 1,
|
||||
depth + 1
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
|
||||
@@ -101,7 +101,7 @@ export async function createClient(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid subnet format. Please provide a valid CIDR notation."
|
||||
"Invalid subnet format. Please provide a valid IP."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ import {
|
||||
verifyUserHasAction,
|
||||
verifyUserIsOrgOwner,
|
||||
verifySiteResourceAccess,
|
||||
verifyOlmAccess
|
||||
verifyOlmAccess,
|
||||
verifyLimits
|
||||
} from "@server/middlewares";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||
@@ -79,6 +80,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/org/:orgId",
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateOrg),
|
||||
logActionAudit(ActionsEnum.updateOrg),
|
||||
org.updateOrg
|
||||
@@ -161,6 +163,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/org/:orgId/client",
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createClient),
|
||||
logActionAudit(ActionsEnum.createClient),
|
||||
client.createClient
|
||||
@@ -178,6 +181,7 @@ authenticated.delete(
|
||||
authenticated.post(
|
||||
"/client/:clientId/archive",
|
||||
verifyClientAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.archiveClient),
|
||||
logActionAudit(ActionsEnum.archiveClient),
|
||||
client.archiveClient
|
||||
@@ -186,6 +190,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
"/client/:clientId/unarchive",
|
||||
verifyClientAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.unarchiveClient),
|
||||
logActionAudit(ActionsEnum.unarchiveClient),
|
||||
client.unarchiveClient
|
||||
@@ -194,6 +199,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
"/client/:clientId/block",
|
||||
verifyClientAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.blockClient),
|
||||
logActionAudit(ActionsEnum.blockClient),
|
||||
client.blockClient
|
||||
@@ -202,6 +208,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
"/client/:clientId/unblock",
|
||||
verifyClientAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.unblockClient),
|
||||
logActionAudit(ActionsEnum.unblockClient),
|
||||
client.unblockClient
|
||||
@@ -210,6 +217,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
"/client/:clientId",
|
||||
verifyClientAccess, // this will check if the user has access to the client
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client
|
||||
logActionAudit(ActionsEnum.updateClient),
|
||||
client.updateClient
|
||||
@@ -224,6 +232,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
"/site/:siteId",
|
||||
verifySiteAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateSite),
|
||||
logActionAudit(ActionsEnum.updateSite),
|
||||
site.updateSite
|
||||
@@ -273,6 +282,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/org/:orgId/site-resource",
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createSiteResource),
|
||||
logActionAudit(ActionsEnum.createSiteResource),
|
||||
siteResource.createSiteResource
|
||||
@@ -303,6 +313,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId",
|
||||
verifySiteResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateSiteResource),
|
||||
logActionAudit(ActionsEnum.updateSiteResource),
|
||||
siteResource.updateSiteResource
|
||||
@@ -341,6 +352,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/roles",
|
||||
verifySiteResourceAccess,
|
||||
verifyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourceRoles),
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
siteResource.setSiteResourceRoles
|
||||
@@ -350,6 +362,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/users",
|
||||
verifySiteResourceAccess,
|
||||
verifySetResourceUsers,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.setSiteResourceUsers
|
||||
@@ -359,6 +372,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/clients",
|
||||
verifySiteResourceAccess,
|
||||
verifySetResourceClients,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.setSiteResourceClients
|
||||
@@ -368,6 +382,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/clients/add",
|
||||
verifySiteResourceAccess,
|
||||
verifySetResourceClients,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.addClientToSiteResource
|
||||
@@ -377,6 +392,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/clients/remove",
|
||||
verifySiteResourceAccess,
|
||||
verifySetResourceClients,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.removeClientFromSiteResource
|
||||
@@ -385,6 +401,7 @@ authenticated.post(
|
||||
authenticated.put(
|
||||
"/org/:orgId/resource",
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createResource),
|
||||
logActionAudit(ActionsEnum.createResource),
|
||||
resource.createResource
|
||||
@@ -499,6 +516,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateResource),
|
||||
logActionAudit(ActionsEnum.updateResource),
|
||||
resource.updateResource
|
||||
@@ -514,6 +532,7 @@ authenticated.delete(
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/target",
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createTarget),
|
||||
logActionAudit(ActionsEnum.createTarget),
|
||||
target.createTarget
|
||||
@@ -528,6 +547,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/rule",
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createResourceRule),
|
||||
logActionAudit(ActionsEnum.createResourceRule),
|
||||
resource.createResourceRule
|
||||
@@ -541,6 +561,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateResourceRule),
|
||||
logActionAudit(ActionsEnum.updateResourceRule),
|
||||
resource.updateResourceRule
|
||||
@@ -562,6 +583,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/target/:targetId",
|
||||
verifyTargetAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateTarget),
|
||||
logActionAudit(ActionsEnum.updateTarget),
|
||||
target.updateTarget
|
||||
@@ -577,6 +599,7 @@ authenticated.delete(
|
||||
authenticated.put(
|
||||
"/org/:orgId/role",
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createRole),
|
||||
logActionAudit(ActionsEnum.createRole),
|
||||
role.createRole
|
||||
@@ -591,6 +614,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/role/:roleId",
|
||||
verifyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateRole),
|
||||
logActionAudit(ActionsEnum.updateRole),
|
||||
role.updateRole
|
||||
@@ -619,6 +643,7 @@ authenticated.post(
|
||||
"/role/:roleId/add/:userId",
|
||||
verifyRoleAccess,
|
||||
verifyUserAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.addUserRole),
|
||||
logActionAudit(ActionsEnum.addUserRole),
|
||||
user.addUserRole
|
||||
@@ -628,6 +653,7 @@ authenticated.post(
|
||||
"/resource/:resourceId/roles",
|
||||
verifyResourceAccess,
|
||||
verifyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourceRoles),
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
resource.setResourceRoles
|
||||
@@ -637,6 +663,7 @@ authenticated.post(
|
||||
"/resource/:resourceId/users",
|
||||
verifyResourceAccess,
|
||||
verifySetResourceUsers,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
resource.setResourceUsers
|
||||
@@ -645,6 +672,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/password`,
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePassword),
|
||||
logActionAudit(ActionsEnum.setResourcePassword),
|
||||
resource.setResourcePassword
|
||||
@@ -653,6 +681,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/pincode`,
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePincode),
|
||||
logActionAudit(ActionsEnum.setResourcePincode),
|
||||
resource.setResourcePincode
|
||||
@@ -661,6 +690,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/header-auth`,
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourceHeaderAuth),
|
||||
logActionAudit(ActionsEnum.setResourceHeaderAuth),
|
||||
resource.setResourceHeaderAuth
|
||||
@@ -669,6 +699,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setResourceWhitelist),
|
||||
logActionAudit(ActionsEnum.setResourceWhitelist),
|
||||
resource.setResourceWhitelist
|
||||
@@ -684,6 +715,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/access-token`,
|
||||
verifyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.generateAccessToken),
|
||||
logActionAudit(ActionsEnum.generateAccessToken),
|
||||
accessToken.generateAccessToken
|
||||
@@ -774,6 +806,7 @@ authenticated.delete(
|
||||
authenticated.put(
|
||||
"/org/:orgId/user",
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createOrgUser),
|
||||
logActionAudit(ActionsEnum.createOrgUser),
|
||||
user.createOrgUser
|
||||
@@ -783,6 +816,7 @@ authenticated.post(
|
||||
"/org/:orgId/user/:userId",
|
||||
verifyOrgAccess,
|
||||
verifyUserAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.updateOrgUser),
|
||||
logActionAudit(ActionsEnum.updateOrgUser),
|
||||
user.updateOrgUser
|
||||
@@ -855,6 +889,7 @@ authenticated.post(
|
||||
"/user/:userId/olm/:olmId/archive",
|
||||
verifyIsLoggedInUser,
|
||||
verifyOlmAccess,
|
||||
verifyLimits,
|
||||
olm.archiveUserOlm
|
||||
);
|
||||
|
||||
@@ -969,6 +1004,7 @@ authenticated.post(
|
||||
`/org/:orgId/api-key/:apiKeyId/actions`,
|
||||
verifyOrgAccess,
|
||||
verifyApiKeyAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.setApiKeyActions),
|
||||
logActionAudit(ActionsEnum.setApiKeyActions),
|
||||
apiKeys.setApiKeyActions
|
||||
@@ -985,6 +1021,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
`/org/:orgId/api-key`,
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createApiKey),
|
||||
logActionAudit(ActionsEnum.createApiKey),
|
||||
apiKeys.createOrgApiKey
|
||||
@@ -1010,6 +1047,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
`/org/:orgId/domain`,
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.createOrgDomain),
|
||||
logActionAudit(ActionsEnum.createOrgDomain),
|
||||
domain.createOrgDomain
|
||||
@@ -1019,6 +1057,7 @@ authenticated.post(
|
||||
`/org/:orgId/domain/:domainId/restart`,
|
||||
verifyOrgAccess,
|
||||
verifyDomainAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.restartOrgDomain),
|
||||
logActionAudit(ActionsEnum.restartOrgDomain),
|
||||
domain.restartOrgDomain
|
||||
@@ -1065,6 +1104,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/org/:orgId/blueprint",
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.applyBlueprint),
|
||||
blueprints.applyYAMLBlueprint
|
||||
);
|
||||
|
||||
@@ -93,7 +93,9 @@ export async function createOidcIdp(
|
||||
name,
|
||||
autoProvision,
|
||||
type: "oidc",
|
||||
tags
|
||||
tags,
|
||||
defaultOrgMapping: `'{{orgId}}'`,
|
||||
defaultRoleMapping: `'Member'`
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import config from "@server/lib/config";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import { build } from "@server/build";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
@@ -112,7 +113,10 @@ export async function generateOidcUrl(
|
||||
}
|
||||
|
||||
if (build === "saas") {
|
||||
const subscribed = await isSubscribed(orgId);
|
||||
const subscribed = await isSubscribed(
|
||||
orgId,
|
||||
tierMatrix.orgOidc
|
||||
);
|
||||
if (!subscribed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
||||
@@ -34,6 +34,8 @@ import { FeatureId } from "@server/lib/billing";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { build } from "@server/build";
|
||||
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const ensureTrailingSlash = (url: string): string => {
|
||||
return url;
|
||||
@@ -326,6 +328,33 @@ export async function validateOidcCallback(
|
||||
.where(eq(idpOrg.idpId, existingIdp.idp.idpId))
|
||||
.innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId));
|
||||
allOrgs = idpOrgs.map((o) => o.orgs);
|
||||
|
||||
// TODO: when there are multiple orgs we need to do this better!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1
|
||||
if (allOrgs.length > 1) {
|
||||
// for some reason there is more than one org
|
||||
logger.error(
|
||||
"More than one organization linked to this IdP. This should not happen with auto-provisioning enabled."
|
||||
);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Multiple organizations linked to this IdP. Please contact support."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const subscribed = await isSubscribed(
|
||||
allOrgs[0].orgId,
|
||||
tierMatrix.autoProvisioning
|
||||
);
|
||||
if (subscribed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"This organization's current plan does not support this feature."
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
allOrgs = await db.select().from(orgs);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyClientAccess,
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceClients
|
||||
verifyApiKeySetResourceClients,
|
||||
verifyLimits
|
||||
} from "@server/middlewares";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { Router } from "express";
|
||||
@@ -74,6 +75,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/org/:orgId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateOrg),
|
||||
logActionAudit(ActionsEnum.updateOrg),
|
||||
org.updateOrg
|
||||
@@ -90,6 +92,7 @@ authenticated.delete(
|
||||
authenticated.put(
|
||||
"/org/:orgId/site",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createSite),
|
||||
logActionAudit(ActionsEnum.createSite),
|
||||
site.createSite
|
||||
@@ -126,6 +129,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/site/:siteId",
|
||||
verifyApiKeySiteAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateSite),
|
||||
logActionAudit(ActionsEnum.updateSite),
|
||||
site.updateSite
|
||||
@@ -146,8 +150,9 @@ authenticated.get(
|
||||
);
|
||||
// Site Resource endpoints
|
||||
authenticated.put(
|
||||
"/org/:orgId/private-resource",
|
||||
"/org/:orgId/site-resource",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
|
||||
logActionAudit(ActionsEnum.createSiteResource),
|
||||
siteResource.createSiteResource
|
||||
@@ -178,6 +183,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/site-resource/:siteResourceId",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
|
||||
logActionAudit(ActionsEnum.updateSiteResource),
|
||||
siteResource.updateSiteResource
|
||||
@@ -216,6 +222,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/roles",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
siteResource.setSiteResourceRoles
|
||||
@@ -225,6 +232,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/users",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.setSiteResourceUsers
|
||||
@@ -234,6 +242,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/roles/add",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
siteResource.addRoleToSiteResource
|
||||
@@ -243,6 +252,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/roles/remove",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
siteResource.removeRoleFromSiteResource
|
||||
@@ -252,6 +262,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/users/add",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.addUserToSiteResource
|
||||
@@ -261,6 +272,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/users/remove",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.removeUserFromSiteResource
|
||||
@@ -270,6 +282,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/clients",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceClients,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.setSiteResourceClients
|
||||
@@ -279,6 +292,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/clients/add",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceClients,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.addClientToSiteResource
|
||||
@@ -288,6 +302,7 @@ authenticated.post(
|
||||
"/site-resource/:siteResourceId/clients/remove",
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeySetResourceClients,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
siteResource.removeClientFromSiteResource
|
||||
@@ -296,6 +311,7 @@ authenticated.post(
|
||||
authenticated.put(
|
||||
"/org/:orgId/resource",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||
logActionAudit(ActionsEnum.createResource),
|
||||
resource.createResource
|
||||
@@ -304,6 +320,7 @@ authenticated.put(
|
||||
authenticated.put(
|
||||
"/org/:orgId/site/:siteId/resource",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||
logActionAudit(ActionsEnum.createResource),
|
||||
resource.createResource
|
||||
@@ -340,6 +357,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/org/:orgId/create-invite",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.inviteUser),
|
||||
logActionAudit(ActionsEnum.inviteUser),
|
||||
user.inviteUser
|
||||
@@ -377,6 +395,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResource),
|
||||
logActionAudit(ActionsEnum.updateResource),
|
||||
resource.updateResource
|
||||
@@ -393,6 +412,7 @@ authenticated.delete(
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/target",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createTarget),
|
||||
logActionAudit(ActionsEnum.createTarget),
|
||||
target.createTarget
|
||||
@@ -408,6 +428,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/rule",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResourceRule),
|
||||
logActionAudit(ActionsEnum.createResourceRule),
|
||||
resource.createResourceRule
|
||||
@@ -423,6 +444,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
|
||||
logActionAudit(ActionsEnum.updateResourceRule),
|
||||
resource.updateResourceRule
|
||||
@@ -446,6 +468,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/target/:targetId",
|
||||
verifyApiKeyTargetAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateTarget),
|
||||
logActionAudit(ActionsEnum.updateTarget),
|
||||
target.updateTarget
|
||||
@@ -462,6 +485,7 @@ authenticated.delete(
|
||||
authenticated.put(
|
||||
"/org/:orgId/role",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createRole),
|
||||
logActionAudit(ActionsEnum.createRole),
|
||||
role.createRole
|
||||
@@ -470,6 +494,7 @@ authenticated.put(
|
||||
authenticated.post(
|
||||
"/role/:roleId",
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateRole),
|
||||
logActionAudit(ActionsEnum.updateRole),
|
||||
role.updateRole
|
||||
@@ -501,6 +526,7 @@ authenticated.post(
|
||||
"/role/:roleId/add/:userId",
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.addUserRole),
|
||||
logActionAudit(ActionsEnum.addUserRole),
|
||||
user.addUserRole
|
||||
@@ -510,6 +536,7 @@ authenticated.post(
|
||||
"/resource/:resourceId/roles",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
resource.setResourceRoles
|
||||
@@ -519,6 +546,7 @@ authenticated.post(
|
||||
"/resource/:resourceId/users",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
resource.setResourceUsers
|
||||
@@ -528,6 +556,7 @@ authenticated.post(
|
||||
"/resource/:resourceId/roles/add",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
resource.addRoleToResource
|
||||
@@ -537,6 +566,7 @@ authenticated.post(
|
||||
"/resource/:resourceId/roles/remove",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
resource.removeRoleFromResource
|
||||
@@ -546,6 +576,7 @@ authenticated.post(
|
||||
"/resource/:resourceId/users/add",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
resource.addUserToResource
|
||||
@@ -555,6 +586,7 @@ authenticated.post(
|
||||
"/resource/:resourceId/users/remove",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
resource.removeUserFromResource
|
||||
@@ -563,6 +595,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/password`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
|
||||
logActionAudit(ActionsEnum.setResourcePassword),
|
||||
resource.setResourcePassword
|
||||
@@ -571,6 +604,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/pincode`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
|
||||
logActionAudit(ActionsEnum.setResourcePincode),
|
||||
resource.setResourcePincode
|
||||
@@ -579,6 +613,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/header-auth`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth),
|
||||
logActionAudit(ActionsEnum.setResourceHeaderAuth),
|
||||
resource.setResourceHeaderAuth
|
||||
@@ -587,6 +622,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
logActionAudit(ActionsEnum.setResourceWhitelist),
|
||||
resource.setResourceWhitelist
|
||||
@@ -595,6 +631,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist/add`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
resource.addEmailToResourceWhitelist
|
||||
);
|
||||
@@ -602,6 +639,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist/remove`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
resource.removeEmailFromResourceWhitelist
|
||||
);
|
||||
@@ -616,6 +654,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/access-token`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
|
||||
logActionAudit(ActionsEnum.generateAccessToken),
|
||||
accessToken.generateAccessToken
|
||||
@@ -653,6 +692,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
"/user/:userId/2fa",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateUser),
|
||||
logActionAudit(ActionsEnum.updateUser),
|
||||
user.updateUser2FA
|
||||
@@ -675,6 +715,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/org/:orgId/user",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createOrgUser),
|
||||
logActionAudit(ActionsEnum.createOrgUser),
|
||||
user.createOrgUser
|
||||
@@ -684,6 +725,7 @@ authenticated.post(
|
||||
"/org/:orgId/user/:userId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateOrgUser),
|
||||
logActionAudit(ActionsEnum.updateOrgUser),
|
||||
user.updateOrgUser
|
||||
@@ -714,6 +756,7 @@ authenticated.get(
|
||||
authenticated.post(
|
||||
`/org/:orgId/api-key/:apiKeyId/actions`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.setApiKeyActions),
|
||||
logActionAudit(ActionsEnum.setApiKeyActions),
|
||||
apiKeys.setApiKeyActions
|
||||
@@ -729,6 +772,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
`/org/:orgId/api-key`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createApiKey),
|
||||
logActionAudit(ActionsEnum.createApiKey),
|
||||
apiKeys.createOrgApiKey
|
||||
@@ -745,6 +789,7 @@ authenticated.delete(
|
||||
authenticated.put(
|
||||
"/idp/oidc",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdp),
|
||||
logActionAudit(ActionsEnum.createIdp),
|
||||
idp.createOidcIdp
|
||||
@@ -753,6 +798,7 @@ authenticated.put(
|
||||
authenticated.post(
|
||||
"/idp/:idpId/oidc",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdp),
|
||||
logActionAudit(ActionsEnum.updateIdp),
|
||||
idp.updateOidcIdp
|
||||
@@ -776,6 +822,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdpOrg),
|
||||
logActionAudit(ActionsEnum.createIdpOrg),
|
||||
idp.createIdpOrgPolicy
|
||||
@@ -784,6 +831,7 @@ authenticated.put(
|
||||
authenticated.post(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdpOrg),
|
||||
logActionAudit(ActionsEnum.updateIdpOrg),
|
||||
idp.updateIdpOrgPolicy
|
||||
@@ -828,6 +876,7 @@ authenticated.get(
|
||||
authenticated.put(
|
||||
"/org/:orgId/client",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.createClient),
|
||||
logActionAudit(ActionsEnum.createClient),
|
||||
client.createClient
|
||||
@@ -854,6 +903,7 @@ authenticated.delete(
|
||||
authenticated.post(
|
||||
"/client/:clientId/archive",
|
||||
verifyApiKeyClientAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.archiveClient),
|
||||
logActionAudit(ActionsEnum.archiveClient),
|
||||
client.archiveClient
|
||||
@@ -862,6 +912,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
"/client/:clientId/unarchive",
|
||||
verifyApiKeyClientAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.unarchiveClient),
|
||||
logActionAudit(ActionsEnum.unarchiveClient),
|
||||
client.unarchiveClient
|
||||
@@ -870,6 +921,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
"/client/:clientId/block",
|
||||
verifyApiKeyClientAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.blockClient),
|
||||
logActionAudit(ActionsEnum.blockClient),
|
||||
client.blockClient
|
||||
@@ -878,6 +930,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
"/client/:clientId/unblock",
|
||||
verifyApiKeyClientAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.unblockClient),
|
||||
logActionAudit(ActionsEnum.unblockClient),
|
||||
client.unblockClient
|
||||
@@ -886,6 +939,7 @@ authenticated.post(
|
||||
authenticated.post(
|
||||
"/client/:clientId",
|
||||
verifyApiKeyClientAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateClient),
|
||||
logActionAudit(ActionsEnum.updateClient),
|
||||
client.updateClient
|
||||
@@ -894,6 +948,7 @@ authenticated.post(
|
||||
authenticated.put(
|
||||
"/org/:orgId/blueprint",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyLimits,
|
||||
verifyApiKeyHasAction(ActionsEnum.applyBlueprint),
|
||||
logActionAudit(ActionsEnum.applyBlueprint),
|
||||
blueprints.applyJSONBlueprint
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import { createCertificate } from "#dynamic/routers/certificates/createCertifica
|
||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const updateResourceParamsSchema = z.strictObject({
|
||||
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||
@@ -341,7 +342,7 @@ async function updateHttpResource(
|
||||
headers = null;
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
|
||||
const isLicensed = await isLicensedOrSubscribed(resource.orgId, tierMatrix.maintencePage);
|
||||
if (!isLicensed) {
|
||||
updateData.maintenanceModeEnabled = undefined;
|
||||
updateData.maintenanceModeType = undefined;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const createRoleParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -100,7 +101,7 @@ export async function createRole(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
|
||||
if (!isLicensed) {
|
||||
roleData.requireDeviceApproval = undefined;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const updateRoleParamsSchema = z.strictObject({
|
||||
roleId: z.string().transform(Number).pipe(z.int().positive())
|
||||
@@ -110,7 +111,7 @@ export async function updateRole(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
|
||||
if (!isLicensed) {
|
||||
updateData.requireDeviceApproval = undefined;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { FeatureId } from "@server/lib/billing";
|
||||
import { build } from "@server/build";
|
||||
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
@@ -127,7 +128,10 @@ export async function createOrgUser(
|
||||
);
|
||||
} else if (type === "oidc") {
|
||||
if (build === "saas") {
|
||||
const subscribed = await isSubscribed(orgId);
|
||||
const subscribed = await isSubscribed(
|
||||
orgId,
|
||||
tierMatrix.orgOidc
|
||||
);
|
||||
if (subscribed) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { GetOrgResponse } from "@server/routers/org";
|
||||
import type { ListRolesResponse } from "@server/routers/role";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export interface ApprovalFeedPageProps {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -29,10 +30,9 @@ export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
|
||||
// Fetch roles to check if approvals are enabled
|
||||
let hasApprovalsEnabled = false;
|
||||
const rolesRes = await internal
|
||||
.get<AxiosResponse<ListRolesResponse>>(
|
||||
`/org/${params.orgId}/roles`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
.get<
|
||||
AxiosResponse<ListRolesResponse>
|
||||
>(`/org/${params.orgId}/roles`, await authCookieHeader())
|
||||
.catch((e) => {});
|
||||
|
||||
if (rolesRes && rolesRes.status === 200) {
|
||||
@@ -52,7 +52,7 @@ export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
|
||||
|
||||
<ApprovalsBanner />
|
||||
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert tiers={tierMatrix.deviceApprovals} />
|
||||
|
||||
<OrgProvider org={org}>
|
||||
<div className="container mx-auto max-w-12xl">
|
||||
|
||||
@@ -34,15 +34,7 @@ import {
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import {
|
||||
CreditCard,
|
||||
ExternalLink,
|
||||
Users,
|
||||
Globe,
|
||||
Server,
|
||||
Layout,
|
||||
Check
|
||||
} from "lucide-react";
|
||||
import { CreditCard, ExternalLink, Check } from "lucide-react";
|
||||
import {
|
||||
GetOrgSubscriptionResponse,
|
||||
GetOrgUsageResponse
|
||||
@@ -50,73 +42,49 @@ import {
|
||||
import { useTranslations } from "use-intl";
|
||||
import Link from "next/link";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
import { w } from "@faker-js/faker/dist/airline-DF6RqYmq";
|
||||
import {
|
||||
tier1LimitSet,
|
||||
tier2LimitSet,
|
||||
tier3LimitSet
|
||||
} from "@server/lib/billing/limitSet";
|
||||
import { FeatureId } from "@server/lib/billing/features";
|
||||
|
||||
// Plan tier definitions matching the mockup
|
||||
type PlanId = "free" | "homelab" | "team" | "business" | "enterprise";
|
||||
type PlanId = "starter" | "home" | "team" | "business" | "enterprise";
|
||||
|
||||
interface PlanOption {
|
||||
type PlanOption = {
|
||||
id: PlanId;
|
||||
name: string;
|
||||
price: string;
|
||||
priceDetail?: string;
|
||||
tierType: Tier | null;
|
||||
}
|
||||
|
||||
// Tier limits for display in confirmation dialog
|
||||
interface TierLimits {
|
||||
sites: number;
|
||||
users: number;
|
||||
domains: number;
|
||||
remoteNodes: number;
|
||||
}
|
||||
|
||||
const tierLimits: Record<Tier, TierLimits> = {
|
||||
tier1: {
|
||||
sites: 3,
|
||||
users: 3,
|
||||
domains: 3,
|
||||
remoteNodes: 1
|
||||
},
|
||||
tier2: {
|
||||
sites: 10,
|
||||
users: 150,
|
||||
domains: 250,
|
||||
remoteNodes: 5
|
||||
},
|
||||
tier3: {
|
||||
sites: 10,
|
||||
users: 150,
|
||||
domains: 250,
|
||||
remoteNodes: 5
|
||||
}
|
||||
};
|
||||
|
||||
const planOptions: PlanOption[] = [
|
||||
{
|
||||
id: "free",
|
||||
name: "Free",
|
||||
price: "Free",
|
||||
id: "starter",
|
||||
name: "Starter",
|
||||
price: "Starter",
|
||||
tierType: null
|
||||
},
|
||||
{
|
||||
id: "homelab",
|
||||
name: "Homelab",
|
||||
price: "$15",
|
||||
id: "home",
|
||||
name: "Home",
|
||||
price: "$12.50",
|
||||
priceDetail: "/ month",
|
||||
tierType: "tier1"
|
||||
},
|
||||
{
|
||||
id: "team",
|
||||
name: "Team",
|
||||
price: "$5",
|
||||
price: "$4",
|
||||
priceDetail: "per user / month",
|
||||
tierType: "tier2"
|
||||
},
|
||||
{
|
||||
id: "business",
|
||||
name: "Business",
|
||||
price: "$10",
|
||||
price: "$9",
|
||||
priceDetail: "per user / month",
|
||||
tierType: "tier3"
|
||||
},
|
||||
@@ -128,6 +96,37 @@ const planOptions: PlanOption[] = [
|
||||
}
|
||||
];
|
||||
|
||||
// Tier limits mapping derived from limit sets
|
||||
const tierLimits: Record<
|
||||
Tier,
|
||||
{ users: number; sites: number; domains: number; remoteNodes: number }
|
||||
> = {
|
||||
tier1: {
|
||||
users: tier1LimitSet[FeatureId.USERS]?.value ?? 0,
|
||||
sites: tier1LimitSet[FeatureId.SITES]?.value ?? 0,
|
||||
domains: tier1LimitSet[FeatureId.DOMAINS]?.value ?? 0,
|
||||
remoteNodes: tier1LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0
|
||||
},
|
||||
tier2: {
|
||||
users: tier2LimitSet[FeatureId.USERS]?.value ?? 0,
|
||||
sites: tier2LimitSet[FeatureId.SITES]?.value ?? 0,
|
||||
domains: tier2LimitSet[FeatureId.DOMAINS]?.value ?? 0,
|
||||
remoteNodes: tier2LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0
|
||||
},
|
||||
tier3: {
|
||||
users: tier3LimitSet[FeatureId.USERS]?.value ?? 0,
|
||||
sites: tier3LimitSet[FeatureId.SITES]?.value ?? 0,
|
||||
domains: tier3LimitSet[FeatureId.DOMAINS]?.value ?? 0,
|
||||
remoteNodes: tier3LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0
|
||||
},
|
||||
enterprise: {
|
||||
users: 0, // Custom for enterprise
|
||||
sites: 0, // Custom for enterprise
|
||||
domains: 0, // Custom for enterprise
|
||||
remoteNodes: 0 // Custom for enterprise
|
||||
}
|
||||
};
|
||||
|
||||
export default function BillingPage() {
|
||||
const { org } = useOrgContext();
|
||||
const envContext = useEnvContext();
|
||||
@@ -156,20 +155,18 @@ export default function BillingPage() {
|
||||
|
||||
const [hasSubscription, setHasSubscription] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentTier, setCurrentTier] = useState<
|
||||
Tier | null
|
||||
>(null);
|
||||
const [currentTier, setCurrentTier] = useState<Tier | null>(null);
|
||||
|
||||
// Usage IDs
|
||||
const SITES = "sites";
|
||||
const USERS = "users";
|
||||
const SITES = "sites";
|
||||
const DOMAINS = "domains";
|
||||
const REMOTE_EXIT_NODES = "remoteExitNodes";
|
||||
|
||||
// Confirmation dialog state
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const [pendingTier, setPendingTier] = useState<{
|
||||
tier: Tier,
|
||||
tier: Tier;
|
||||
action: "upgrade" | "downgrade";
|
||||
planName: string;
|
||||
price: string;
|
||||
@@ -195,9 +192,7 @@ export default function BillingPage() {
|
||||
setTierSubscription(tierSub || null);
|
||||
|
||||
if (tierSub?.subscription) {
|
||||
setCurrentTier(
|
||||
tierSub.subscription.type as Tier
|
||||
);
|
||||
setCurrentTier(tierSub.subscription.type as Tier);
|
||||
setHasSubscription(
|
||||
tierSub.subscription.status === "active"
|
||||
);
|
||||
@@ -241,9 +236,7 @@ export default function BillingPage() {
|
||||
fetchUsage();
|
||||
}, [org.org.orgId]);
|
||||
|
||||
const handleStartSubscription = async (
|
||||
tier: Tier
|
||||
) => {
|
||||
const handleStartSubscription = async (tier: Tier) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await api.post<AxiosResponse<string>>(
|
||||
@@ -311,8 +304,70 @@ export default function BillingPage() {
|
||||
await api.post(`/org/${org.org.orgId}/billing/change-tier`, {
|
||||
tier
|
||||
});
|
||||
// Refresh subscription data
|
||||
window.location.reload();
|
||||
|
||||
// Poll the API to check if the tier change has been reflected
|
||||
const pollForTierChange = async (targetTier: Tier) => {
|
||||
const maxAttempts = 30; // 30 seconds with 1 second interval
|
||||
let attempts = 0;
|
||||
|
||||
const poll = async (): Promise<boolean> => {
|
||||
try {
|
||||
const res = await api.get<
|
||||
AxiosResponse<GetOrgSubscriptionResponse>
|
||||
>(`/org/${org.org.orgId}/billing/subscriptions`);
|
||||
const { subscriptions } = res.data.data;
|
||||
|
||||
// Find tier subscription
|
||||
const tierSub = subscriptions.find(
|
||||
({ subscription }) =>
|
||||
subscription?.type === "tier1" ||
|
||||
subscription?.type === "tier2" ||
|
||||
subscription?.type === "tier3"
|
||||
);
|
||||
|
||||
// Check if the tier has changed to the target tier
|
||||
if (tierSub?.subscription?.type === targetTier) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error polling subscription:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
const success = await poll();
|
||||
|
||||
if (success) {
|
||||
// Tier change reflected, refresh the page
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
// Wait 1 second before next poll
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 1000)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If we've exhausted all attempts, show an error
|
||||
toast({
|
||||
title: "Tier change processing",
|
||||
description:
|
||||
"Your tier change is taking longer than expected. Please refresh the page in a moment to see the changes.",
|
||||
variant: "destructive"
|
||||
});
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// Start polling for the tier change
|
||||
pollForTierChange(tier);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Failed to change tier",
|
||||
@@ -337,8 +392,8 @@ export default function BillingPage() {
|
||||
}
|
||||
}
|
||||
|
||||
setShowConfirmDialog(false);
|
||||
setPendingTier(null);
|
||||
// setShowConfirmDialog(false);
|
||||
// setPendingTier(null);
|
||||
};
|
||||
|
||||
const showTierConfirmation = (
|
||||
@@ -352,14 +407,14 @@ export default function BillingPage() {
|
||||
};
|
||||
|
||||
const handleContactUs = () => {
|
||||
window.open("mailto:sales@pangolin.net", "_blank");
|
||||
window.open("https://pangolin.net/talk-to-us", "_blank");
|
||||
};
|
||||
|
||||
// Get current plan ID from tier
|
||||
const getCurrentPlanId = (): PlanId => {
|
||||
if (!hasSubscription || !currentTier) return "free";
|
||||
if (!hasSubscription || !currentTier) return "starter";
|
||||
const plan = planOptions.find((p) => p.tierType === currentTier);
|
||||
return plan?.id || "free";
|
||||
return plan?.id || "starter";
|
||||
};
|
||||
|
||||
const currentPlanId = getCurrentPlanId();
|
||||
@@ -376,8 +431,8 @@ export default function BillingPage() {
|
||||
}
|
||||
|
||||
if (plan.id === currentPlanId) {
|
||||
// If it's the free plan (free with no subscription), show as current but disabled
|
||||
if (plan.id === "free" && !hasSubscription) {
|
||||
// If it's the starter plan (starter with no subscription), show as current but disabled
|
||||
if (plan.id === "starter" && !hasSubscription) {
|
||||
return {
|
||||
label: "Current Plan",
|
||||
action: () => {},
|
||||
@@ -407,7 +462,7 @@ export default function BillingPage() {
|
||||
plan.tierType,
|
||||
"downgrade",
|
||||
plan.name,
|
||||
plan.price + (plan.priceDetail || "")
|
||||
plan.price + (" " + plan.priceDetail || "")
|
||||
);
|
||||
} else {
|
||||
handleModifySubscription();
|
||||
@@ -426,7 +481,7 @@ export default function BillingPage() {
|
||||
plan.tierType,
|
||||
"upgrade",
|
||||
plan.name,
|
||||
plan.price + (plan.priceDetail || "")
|
||||
plan.price + (" " + plan.priceDetail || "")
|
||||
);
|
||||
} else {
|
||||
handleModifySubscription();
|
||||
@@ -452,14 +507,15 @@ export default function BillingPage() {
|
||||
// Calculate current usage cost for display
|
||||
const getUserCount = () => getUsageValue(USERS);
|
||||
const getPricePerUser = () => {
|
||||
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
|
||||
(item) => item.featureId === USERS
|
||||
);
|
||||
|
||||
console.log("Users subscription item:", usersItem);
|
||||
|
||||
// unitAmount is in cents, convert to dollars
|
||||
if (usersItem?.unitAmount) {
|
||||
return usersItem.unitAmount / 100;
|
||||
@@ -540,6 +596,7 @@ export default function BillingPage() {
|
||||
disabled={
|
||||
isLoading || planAction.disabled
|
||||
}
|
||||
loading={isLoading && isCurrentPlan}
|
||||
>
|
||||
{planAction.label}
|
||||
</Button>
|
||||
@@ -747,9 +804,9 @@ export default function BillingPage() {
|
||||
<span>
|
||||
{
|
||||
tierLimits[pendingTier.tier]
|
||||
.sites
|
||||
.users
|
||||
}{" "}
|
||||
{t("billingSites") || "Sites"}
|
||||
{t("billingUsers") || "Users"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -757,9 +814,9 @@ export default function BillingPage() {
|
||||
<span>
|
||||
{
|
||||
tierLimits[pendingTier.tier]
|
||||
.users
|
||||
.sites
|
||||
}{" "}
|
||||
{t("billingUsers") || "Users"}
|
||||
{t("billingSites") || "Sites"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -798,14 +855,13 @@ export default function BillingPage() {
|
||||
<Button
|
||||
onClick={confirmTierChange}
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? t("billingProcessing") || "Processing..."
|
||||
: pendingTier?.action === "upgrade"
|
||||
? t("billingConfirmUpgradeButton") ||
|
||||
"Confirm Upgrade"
|
||||
: t("billingConfirmDowngradeButton") ||
|
||||
"Confirm Downgrade"}
|
||||
{pendingTier?.action === "upgrade"
|
||||
? t("billingConfirmUpgradeButton") ||
|
||||
"Confirm Upgrade"
|
||||
: t("billingConfirmDowngradeButton") ||
|
||||
"Confirm Downgrade"}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -31,7 +31,6 @@ import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useState, useEffect } from "react";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon, ExternalLink } from "lucide-react";
|
||||
import {
|
||||
@@ -41,12 +40,13 @@ import {
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { ListRolesResponse } from "@server/routers/role";
|
||||
import AutoProvisionConfigWidget from "@app/components/private/AutoProvisionConfigWidget";
|
||||
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -60,7 +60,6 @@ export default function GeneralPage() {
|
||||
"role" | "expression"
|
||||
>("role");
|
||||
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
||||
const [redirectUrl, setRedirectUrl] = useState(
|
||||
@@ -499,6 +498,10 @@ export default function GeneralPage() {
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.autoProvisioning}
|
||||
/>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import AutoProvisionConfigWidget from "@app/components/private/AutoProvisionConfigWidget";
|
||||
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
@@ -31,6 +32,7 @@ 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";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { ListRolesResponse } from "@server/routers/role";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
@@ -50,7 +52,6 @@ export default function Page() {
|
||||
const [roleMappingMode, setRoleMappingMode] = useState<
|
||||
"role" | "expression"
|
||||
>("role");
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
const t = useTranslations();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
@@ -363,6 +364,9 @@ export default function Page() {
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.autoProvisioning}
|
||||
/>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
@@ -808,7 +812,7 @@ export default function Page() {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createLoading || !isPaidUser}
|
||||
disabled={createLoading || !isPaidUser(tierMatrix.orgOidc)}
|
||||
loading={createLoading}
|
||||
onClick={() => {
|
||||
// log any issues with the form
|
||||
|
||||
@@ -2,9 +2,10 @@ import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { AxiosResponse } from "axios";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import IdpTable, { IdpRow } from "@app/components/private/OrgIdpTable";
|
||||
import IdpTable, { IdpRow } from "@app/components/OrgIdpTable";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type OrgIdpPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -35,7 +36,7 @@ export default async function OrgIdpPage(props: OrgIdpPageProps) {
|
||||
description={t("idpManageDescription")}
|
||||
/>
|
||||
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert tiers={tierMatrix.orgOidc} />
|
||||
|
||||
<IdpTable idps={idps} orgId={params.orgId} />
|
||||
</>
|
||||
|
||||
@@ -23,9 +23,6 @@ import {
|
||||
} from "@server/routers/remoteExitNode/types";
|
||||
import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
@@ -36,6 +33,8 @@ import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -45,6 +44,8 @@ export default function CredentialsPage() {
|
||||
const t = useTranslations();
|
||||
const { remoteExitNode } = useRemoteExitNodeContext();
|
||||
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [credentials, setCredentials] =
|
||||
useState<PickRemoteExitNodeDefaultsResponse | null>(null);
|
||||
@@ -57,16 +58,6 @@ export default function CredentialsPage() {
|
||||
const [showCredentialsAlert, setShowCredentialsAlert] = useState(false);
|
||||
const [shouldDisconnect, setShouldDisconnect] = useState(true);
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
};
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
try {
|
||||
const response = await api.get<
|
||||
@@ -138,7 +129,9 @@ export default function CredentialsPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.rotateCredentials}
|
||||
/>
|
||||
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
@@ -203,7 +196,9 @@ export default function CredentialsPage() {
|
||||
setShouldDisconnect(false);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(tierMatrix.rotateCredentials)
|
||||
}
|
||||
>
|
||||
{t("regenerateCredentialsButton")}
|
||||
</Button>
|
||||
@@ -212,7 +207,9 @@ export default function CredentialsPage() {
|
||||
setShouldDisconnect(true);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(tierMatrix.rotateCredentials)
|
||||
}
|
||||
>
|
||||
{t("remoteExitNodeRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
|
||||
@@ -47,7 +47,8 @@ import { ListIdpsResponse } from "@server/routers/idp";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
import Image from "next/image";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type UserType = "internal" | "oidc";
|
||||
|
||||
@@ -75,7 +76,7 @@ export default function Page() {
|
||||
const api = createApiClient({ env });
|
||||
const t = useTranslations();
|
||||
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const { hasSaasSubscription } = usePaidStatus();
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<string | null>(
|
||||
"internal"
|
||||
@@ -237,7 +238,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
async function fetchIdps() {
|
||||
if (build === "saas" && !subscription?.subscribed) {
|
||||
if (build === "saas" && !hasSaasSubscription(tierMatrix.orgOidc)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,6 @@ import { useTranslations } from "next-intl";
|
||||
import { PickClientDefaultsResponse } from "@server/routers/client";
|
||||
import { useClientContext } from "@app/hooks/useClientContext";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
@@ -33,6 +30,8 @@ import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { OlmInstallCommands } from "@app/components/olm-install-commands";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -54,17 +53,7 @@ export default function CredentialsPage() {
|
||||
const [showCredentialsAlert, setShowCredentialsAlert] = useState(false);
|
||||
const [shouldDisconnect, setShouldDisconnect] = useState(true);
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return (
|
||||
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
|
||||
);
|
||||
};
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
try {
|
||||
@@ -130,7 +119,9 @@ export default function CredentialsPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.rotateCredentials}
|
||||
/>
|
||||
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
@@ -191,7 +182,9 @@ export default function CredentialsPage() {
|
||||
setShouldDisconnect(false);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(tierMatrix.rotateCredentials)
|
||||
}
|
||||
>
|
||||
{t("regenerateCredentialsButton")}
|
||||
</Button>
|
||||
@@ -200,7 +193,9 @@ export default function CredentialsPage() {
|
||||
setShouldDisconnect(true);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(tierMatrix.rotateCredentials)
|
||||
}
|
||||
>
|
||||
{t("clientRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
|
||||
@@ -155,13 +155,11 @@ export default function GeneralPage() {
|
||||
const [, startTransition] = useTransition();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const showApprovalFeatures = build !== "oss" && isPaidUser;
|
||||
const showApprovalFeatures =
|
||||
build !== "oss" && isPaidUser(tierMatrix.deviceApprovals);
|
||||
|
||||
const formatPostureValue = (
|
||||
value: boolean | null | undefined | "-"
|
||||
) => {
|
||||
if (value === null || value === undefined || value === "-")
|
||||
return "-";
|
||||
const formatPostureValue = (value: boolean | null | undefined | "-") => {
|
||||
if (value === null || value === undefined || value === "-") return "-";
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{value ? (
|
||||
@@ -584,7 +582,8 @@ export default function GeneralPage() {
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert tiers={tierMatrix.devicePosture} />
|
||||
|
||||
{client.posture &&
|
||||
Object.keys(client.posture).length > 0 ? (
|
||||
<>
|
||||
@@ -598,7 +597,9 @@ export default function GeneralPage() {
|
||||
{t("biometricsEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser(tierMatrix.devicePosture)
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.biometricsEnabled
|
||||
@@ -616,7 +617,9 @@ export default function GeneralPage() {
|
||||
{t("diskEncrypted")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.diskEncrypted
|
||||
@@ -634,7 +637,9 @@ export default function GeneralPage() {
|
||||
{t("firewallEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.firewallEnabled
|
||||
@@ -653,7 +658,9 @@ export default function GeneralPage() {
|
||||
{t("autoUpdatesEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.autoUpdatesEnabled
|
||||
@@ -671,7 +678,9 @@ export default function GeneralPage() {
|
||||
{t("tpmAvailable")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.tpmAvailable
|
||||
@@ -693,7 +702,9 @@ export default function GeneralPage() {
|
||||
)}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.windowsAntivirusEnabled
|
||||
@@ -711,7 +722,9 @@ export default function GeneralPage() {
|
||||
{t("macosSipEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.macosSipEnabled
|
||||
@@ -733,7 +746,9 @@ export default function GeneralPage() {
|
||||
)}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.macosGatekeeperEnabled
|
||||
@@ -755,7 +770,9 @@ export default function GeneralPage() {
|
||||
)}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.macosFirewallStealthMode
|
||||
@@ -774,7 +791,9 @@ export default function GeneralPage() {
|
||||
{t("linuxAppArmorEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.linuxAppArmorEnabled
|
||||
@@ -793,7 +812,9 @@ export default function GeneralPage() {
|
||||
{t("linuxSELinuxEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
{isPaidUser(
|
||||
tierMatrix.devicePosture
|
||||
)
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.linuxSELinuxEnabled
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import AuthPageBrandingForm from "@app/components/AuthPageBrandingForm";
|
||||
import AuthPageSettings from "@app/components/private/AuthPageSettings";
|
||||
import AuthPageSettings from "@app/components/AuthPageSettings";
|
||||
import { SettingsContainer } from "@app/components/Settings";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
|
||||
@@ -43,6 +43,8 @@ import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import type { OrgContextType } from "@app/contexts/orgContext";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { isAppPageRouteDefinition } from "next/dist/server/route-definitions/app-page-route-definition";
|
||||
|
||||
// Session length options in hours
|
||||
const SESSION_LENGTH_OPTIONS = [
|
||||
@@ -244,13 +246,17 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
|
||||
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.accessLogs}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="settingsLogRetentionDaysAccess"
|
||||
render={({ field }) => {
|
||||
const isDisabled = !isPaidUser;
|
||||
const isDisabled = !isPaidUser(
|
||||
tierMatrix.accessLogs
|
||||
);
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
@@ -316,7 +322,9 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
|
||||
control={form.control}
|
||||
name="settingsLogRetentionDaysAction"
|
||||
render={({ field }) => {
|
||||
const isDisabled = !isPaidUser;
|
||||
const isDisabled = !isPaidUser(
|
||||
tierMatrix.actionLogs
|
||||
);
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
@@ -521,12 +529,17 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
|
||||
id="security-settings-section-form"
|
||||
className="space-y-4"
|
||||
>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.twoFactorEnforcement}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requireTwoFactor"
|
||||
render={({ field }) => {
|
||||
const isDisabled = !isPaidUser;
|
||||
const isDisabled = !isPaidUser(
|
||||
tierMatrix.twoFactorEnforcement
|
||||
);
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
@@ -573,7 +586,9 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
|
||||
control={form.control}
|
||||
name="maxSessionLengthHours"
|
||||
render={({ field }) => {
|
||||
const isDisabled = !isPaidUser;
|
||||
const isDisabled = !isPaidUser(
|
||||
tierMatrix.sessionDurationPolicies
|
||||
);
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
@@ -653,7 +668,9 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
|
||||
control={form.control}
|
||||
name="passwordExpiryDays"
|
||||
render={({ field }) => {
|
||||
const isDisabled = !isPaidUser;
|
||||
const isDisabled = !isPaidUser(
|
||||
tierMatrix.passwordExpirationPolicies
|
||||
);
|
||||
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
@@ -739,7 +756,12 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
|
||||
type="submit"
|
||||
form="security-settings-section-form"
|
||||
loading={loadingSave}
|
||||
disabled={loadingSave || !isPaidUser}
|
||||
disabled={
|
||||
loadingSave ||
|
||||
!isPaidUser(tierMatrix.twoFactorEnforcement) ||
|
||||
!isPaidUser(tierMatrix.sessionDurationPolicies) ||
|
||||
!isPaidUser(tierMatrix.passwordExpirationPolicies)
|
||||
}
|
||||
>
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
|
||||
@@ -13,14 +13,13 @@ import { ArrowUpRight, Key, User } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import axios from "axios";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const router = useRouter();
|
||||
@@ -28,8 +27,8 @@ export default function GeneralPage() {
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { orgId } = useParams();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
@@ -208,11 +207,7 @@ export default function GeneralPage() {
|
||||
}
|
||||
) => {
|
||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||
if (
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked()) ||
|
||||
build === "oss"
|
||||
) {
|
||||
if (!isPaidUser(tierMatrix.accessLogs) || build === "oss") {
|
||||
console.log(
|
||||
"Access denied: subscription inactive or license locked"
|
||||
);
|
||||
@@ -613,7 +608,7 @@ export default function GeneralPage() {
|
||||
description={t("accessLogsDescription")}
|
||||
/>
|
||||
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert tiers={tierMatrix.accessLogs} />
|
||||
|
||||
<LogDataTable
|
||||
columns={columns}
|
||||
@@ -623,6 +618,9 @@ export default function GeneralPage() {
|
||||
isRefreshing={isRefreshing}
|
||||
onExport={() => startTransition(exportData)}
|
||||
isExporting={isExporting}
|
||||
isExportDisabled={
|
||||
!isPaidUser(tierMatrix.accessLogs) || build === "oss"
|
||||
}
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
dateRange={{
|
||||
start: dateRange.startDate,
|
||||
@@ -642,11 +640,7 @@ export default function GeneralPage() {
|
||||
// Row expansion props
|
||||
expandable={true}
|
||||
renderExpandedRow={renderExpandedRow}
|
||||
disabled={
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked()) ||
|
||||
build === "oss"
|
||||
}
|
||||
disabled={!isPaidUser(tierMatrix.accessLogs) || build === "oss"}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,15 +4,14 @@ import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import { Key, User } from "lucide-react";
|
||||
@@ -26,8 +25,8 @@ export default function GeneralPage() {
|
||||
const t = useTranslations();
|
||||
const { orgId } = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
@@ -195,10 +194,7 @@ export default function GeneralPage() {
|
||||
}
|
||||
) => {
|
||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||
if (
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked())
|
||||
) {
|
||||
if (!isPaidUser(tierMatrix.actionLogs)) {
|
||||
console.log(
|
||||
"Access denied: subscription inactive or license locked"
|
||||
);
|
||||
@@ -465,7 +461,7 @@ export default function GeneralPage() {
|
||||
description={t("actionLogsDescription")}
|
||||
/>
|
||||
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert tiers={tierMatrix.actionLogs} />
|
||||
|
||||
<LogDataTable
|
||||
columns={columns}
|
||||
@@ -476,6 +472,9 @@ export default function GeneralPage() {
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
onExport={() => startTransition(exportData)}
|
||||
isExportDisabled={
|
||||
!isPaidUser(tierMatrix.logExport) || build === "oss"
|
||||
}
|
||||
isExporting={isExporting}
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
dateRange={{
|
||||
@@ -496,11 +495,7 @@ export default function GeneralPage() {
|
||||
// Row expansion props
|
||||
expandable={true}
|
||||
renderExpandedRow={renderExpandedRow}
|
||||
disabled={
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked()) ||
|
||||
build === "oss"
|
||||
}
|
||||
disabled={!isPaidUser(tierMatrix.actionLogs) || build === "oss"}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,54 +1,3 @@
|
||||
"use client";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import AuthPageSettings, {
|
||||
AuthPageSettingsRef
|
||||
} from "@app/components/private/AuthPageSettings";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useState, useRef } from "react";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { DeleteOrgResponse, ListUserOrgsResponse } from "@server/routers/org";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionFooter
|
||||
} from "@app/components/Settings";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
return <p>dfas</p>;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -50,9 +50,6 @@ import { useActionState, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import z from "zod";
|
||||
import { build } from "@server/build";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||
import {
|
||||
@@ -64,6 +61,7 @@ import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type MaintenanceSectionFormProps = {
|
||||
resource: GetResourceResponse;
|
||||
@@ -77,8 +75,6 @@ function MaintenanceSectionForm({
|
||||
const { env } = useEnvContext();
|
||||
const t = useTranslations();
|
||||
const api = createApiClient({ env });
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const MaintenanceFormSchema = z.object({
|
||||
@@ -159,15 +155,6 @@ function MaintenanceSectionForm({
|
||||
}
|
||||
}
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return (
|
||||
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
|
||||
);
|
||||
};
|
||||
|
||||
if (!resource.http) {
|
||||
return null;
|
||||
}
|
||||
@@ -191,13 +178,15 @@ function MaintenanceSectionForm({
|
||||
className="space-y-4"
|
||||
id="maintenance-settings-form"
|
||||
>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.maintencePage}
|
||||
/>
|
||||
<FormField
|
||||
control={maintenanceForm.control}
|
||||
name="maintenanceModeEnabled"
|
||||
render={({ field }) => {
|
||||
const isDisabled =
|
||||
isSecurityFeatureDisabled() ||
|
||||
!isPaidUser(tierMatrix.maintencePage) ||
|
||||
resource.http === false;
|
||||
|
||||
return (
|
||||
@@ -264,7 +253,11 @@ function MaintenanceSectionForm({
|
||||
defaultValue={
|
||||
field.value
|
||||
}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.maintencePage
|
||||
)
|
||||
}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
<FormItem className="flex items-start space-x-3 space-y-0">
|
||||
@@ -337,7 +330,11 @@ function MaintenanceSectionForm({
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.maintencePage
|
||||
)
|
||||
}
|
||||
placeholder="We'll be back soon!"
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -363,7 +360,11 @@ function MaintenanceSectionForm({
|
||||
<Textarea
|
||||
{...field}
|
||||
rows={4}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.maintencePage
|
||||
)
|
||||
}
|
||||
placeholder={t(
|
||||
"maintenancePageMessagePlaceholder"
|
||||
)}
|
||||
@@ -392,7 +393,11 @@ function MaintenanceSectionForm({
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.maintencePage
|
||||
)
|
||||
}
|
||||
placeholder={t(
|
||||
"maintenanceTime"
|
||||
)}
|
||||
@@ -418,7 +423,10 @@ function MaintenanceSectionForm({
|
||||
<Button
|
||||
type="submit"
|
||||
loading={maintenanceSaveLoading}
|
||||
disabled={maintenanceSaveLoading || !isPaidUser}
|
||||
disabled={
|
||||
maintenanceSaveLoading ||
|
||||
!isPaidUser(tierMatrix.maintencePage)
|
||||
}
|
||||
form="maintenance-settings-form"
|
||||
>
|
||||
{t("saveSettings")}
|
||||
|
||||
@@ -20,9 +20,6 @@ import { PickSiteDefaultsResponse } from "@server/routers/site";
|
||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||
import { generateKeypair } from "../wireguardConfig";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
@@ -40,6 +37,8 @@ import {
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -65,17 +64,7 @@ export default function CredentialsPage() {
|
||||
const [loadingDefaults, setLoadingDefaults] = useState(false);
|
||||
const [shouldDisconnect, setShouldDisconnect] = useState(true);
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return (
|
||||
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
|
||||
);
|
||||
};
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
// Fetch site defaults for wireguard sites to show in obfuscated config
|
||||
useEffect(() => {
|
||||
@@ -207,7 +196,9 @@ export default function CredentialsPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.rotateCredentials}
|
||||
/>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={3}>
|
||||
@@ -279,7 +270,11 @@ export default function CredentialsPage() {
|
||||
setShouldDisconnect(false);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.rotateCredentials
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("regenerateCredentialsButton")}
|
||||
</Button>
|
||||
@@ -288,7 +283,11 @@ export default function CredentialsPage() {
|
||||
setShouldDisconnect(true);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.rotateCredentials
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("siteRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
@@ -315,7 +314,9 @@ export default function CredentialsPage() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.rotateCredentials}
|
||||
/>
|
||||
|
||||
<SettingsSectionBody>
|
||||
{!loadingDefaults && (
|
||||
@@ -389,7 +390,11 @@ export default function CredentialsPage() {
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.rotateCredentials
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("siteRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
LoadLoginPageResponse
|
||||
} from "@server/routers/loginPage/types";
|
||||
import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types";
|
||||
import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken";
|
||||
import ValidateSessionTransferToken from "@app/components/ValidateSessionTransferToken";
|
||||
import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed";
|
||||
import { OrgSelectionForm } from "@app/components/OrgSelectionForm";
|
||||
import OrgLoginPage from "@app/components/OrgLoginPage";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ThemeProvider } from "@app/providers/ThemeProvider";
|
||||
import EnvProvider from "@app/providers/EnvProvider";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import ThemeDataProvider from "@app/providers/ThemeDataProvider";
|
||||
import SplashImage from "@app/components/private/SplashImage";
|
||||
import SplashImage from "@app/components/SplashImage";
|
||||
import SupportStatusProvider from "@app/providers/SupporterStatusProvider";
|
||||
import { priv } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
@@ -35,6 +35,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { build } from "@server/build";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
export type AuthPageCustomizationProps = {
|
||||
orgId: string;
|
||||
@@ -139,14 +140,14 @@ export default function AuthPageBrandingForm({
|
||||
`Choose your preferred authentication method for {{resourceName}}`,
|
||||
primaryColor: branding?.primaryColor ?? `#f36117` // default pangolin primary color
|
||||
},
|
||||
disabled: !isPaidUser
|
||||
disabled: !isPaidUser(tierMatrix.loginPageBranding)
|
||||
});
|
||||
|
||||
async function updateBranding() {
|
||||
const isValid = await form.trigger();
|
||||
const brandingData = form.getValues();
|
||||
|
||||
if (!isValid || !isPaidUser) return;
|
||||
if (!isValid || !isPaidUser(tierMatrix.loginPageBranding)) return;
|
||||
|
||||
try {
|
||||
const updateRes = await api.put(
|
||||
@@ -177,8 +178,6 @@ export default function AuthPageBrandingForm({
|
||||
}
|
||||
|
||||
async function deleteBranding() {
|
||||
if (!isPaidUser) return;
|
||||
|
||||
try {
|
||||
const updateRes = await api.delete(
|
||||
`/org/${orgId}/login-page-branding`
|
||||
@@ -221,7 +220,9 @@ export default function AuthPageBrandingForm({
|
||||
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.loginPageBranding}
|
||||
/>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -436,7 +437,7 @@ export default function AuthPageBrandingForm({
|
||||
disabled={
|
||||
isUpdatingBranding ||
|
||||
isDeletingBranding ||
|
||||
!isPaidUser
|
||||
!isPaidUser(tierMatrix.loginPageBranding)
|
||||
}
|
||||
className="gap-1"
|
||||
>
|
||||
@@ -451,7 +452,7 @@ export default function AuthPageBrandingForm({
|
||||
disabled={
|
||||
isUpdatingBranding ||
|
||||
isDeletingBranding ||
|
||||
!isPaidUser
|
||||
!isPaidUser(tierMatrix.loginPageBranding)
|
||||
}
|
||||
>
|
||||
{t("saveAuthPageBranding")}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { ListDomainsResponse } from "@server/routers/domain";
|
||||
import { DomainRow } from "@app/components/DomainsTable";
|
||||
import { toUnicode } from "punycode";
|
||||
import { Globe, Trash2 } from "lucide-react";
|
||||
import CertificateStatus from "@app/components/private/CertificateStatus";
|
||||
import CertificateStatus from "@app/components/CertificateStatus";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
@@ -42,10 +42,10 @@ import {
|
||||
import DomainPicker from "@app/components/DomainPicker";
|
||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { build } from "@server/build";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { PaidFeaturesAlert } from "../PaidFeaturesAlert";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
// Auth page form schema
|
||||
const AuthPageFormSchema = z.object({
|
||||
@@ -75,7 +75,7 @@ function AuthPageSettings({
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const { hasSaasSubscription } = usePaidStatus();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
// Auth page domain state
|
||||
const [loginPage, setLoginPage] = useState(defaultLoginPage);
|
||||
@@ -177,7 +177,7 @@ function AuthPageSettings({
|
||||
try {
|
||||
// Handle auth page domain
|
||||
if (data.authPageDomainId) {
|
||||
if (build === "enterprise" || hasSaasSubscription) {
|
||||
if (isPaidUser(tierMatrix.loginPageDomain)) {
|
||||
const sanitizedSubdomain = data.authPageSubdomain
|
||||
? finalizeSubdomainSanitize(data.authPageSubdomain)
|
||||
: "";
|
||||
@@ -285,7 +285,7 @@ function AuthPageSettings({
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert tiers={tierMatrix.loginPageDomain} />
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -361,7 +361,11 @@ function AuthPageSettings({
|
||||
onClick={() =>
|
||||
setEditDomainOpen(true)
|
||||
}
|
||||
disabled={!hasSaasSubscription}
|
||||
disabled={
|
||||
!isPaidUser(
|
||||
tierMatrix.loginPageDomain
|
||||
)
|
||||
}
|
||||
>
|
||||
{form.watch("authPageDomainId")
|
||||
? t("changeDomain")
|
||||
@@ -376,7 +380,9 @@ function AuthPageSettings({
|
||||
clearAuthPageDomain
|
||||
}
|
||||
disabled={
|
||||
!hasSaasSubscription
|
||||
!isPaidUser(
|
||||
tierMatrix.loginPageDomain
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 size="14" />
|
||||
@@ -395,7 +401,9 @@ function AuthPageSettings({
|
||||
|
||||
{env.flags.usePangolinDns &&
|
||||
(build === "enterprise" ||
|
||||
!hasSaasSubscription) &&
|
||||
!isPaidUser(
|
||||
tierMatrix.loginPageDomain
|
||||
)) &&
|
||||
loginPage?.domainId &&
|
||||
loginPage?.fullDomain &&
|
||||
!hasUnsavedChanges && (
|
||||
@@ -424,7 +432,7 @@ function AuthPageSettings({
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
!hasUnsavedChanges ||
|
||||
!hasSaasSubscription
|
||||
!isPaidUser(tierMatrix.loginPageDomain)
|
||||
}
|
||||
>
|
||||
{t("saveAuthPageDomain")}
|
||||
@@ -477,7 +485,10 @@ function AuthPageSettings({
|
||||
handleDomainSelection(selectedDomain);
|
||||
}
|
||||
}}
|
||||
disabled={!selectedDomain || !hasSaasSubscription}
|
||||
disabled={
|
||||
!selectedDomain ||
|
||||
!isPaidUser(tierMatrix.loginPageDomain)
|
||||
}
|
||||
>
|
||||
{t("selectDomain")}
|
||||
</Button>
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Control, FieldValues, Path } from "react-hook-form";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type Role = {
|
||||
roleId: number;
|
||||
@@ -49,6 +51,8 @@ export default function AutoProvisionConfigWidget<T extends FieldValues>({
|
||||
}: AutoProvisionConfigWidgetProps<T>) {
|
||||
const t = useTranslations();
|
||||
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="mb-4">
|
||||
@@ -57,6 +61,7 @@ export default function AutoProvisionConfigWidget<T extends FieldValues>({
|
||||
label={t("idpAutoProvisionUsers")}
|
||||
defaultChecked={autoProvision}
|
||||
onCheckedChange={onAutoProvisionChange}
|
||||
disabled={!isPaidUser(tierMatrix.autoProvisioning)}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("idpAutoProvisionUsersDescription")}
|
||||
@@ -36,6 +36,7 @@ import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type CreateRoleFormProps = {
|
||||
open: boolean;
|
||||
@@ -164,7 +165,9 @@ export default function CreateRoleForm({
|
||||
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.deviceApprovals}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -175,7 +178,9 @@ export default function CreateRoleForm({
|
||||
<CheckboxWithLabel
|
||||
{...field}
|
||||
disabled={
|
||||
!isPaidUser
|
||||
!isPaidUser(
|
||||
tierMatrix.deviceApprovals
|
||||
)
|
||||
}
|
||||
value="on"
|
||||
checked={form.watch(
|
||||
|
||||
@@ -42,6 +42,7 @@ import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
|
||||
type CreateRoleFormProps = {
|
||||
role: Role;
|
||||
@@ -172,7 +173,9 @@ export default function EditRoleForm({
|
||||
|
||||
{!env.flags.disableEnterpriseFeatures && (
|
||||
<>
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert
|
||||
tiers={tierMatrix.deviceApprovals}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -183,7 +186,9 @@ export default function EditRoleForm({
|
||||
<CheckboxWithLabel
|
||||
{...field}
|
||||
disabled={
|
||||
!isPaidUser
|
||||
!isPaidUser(
|
||||
tierMatrix.deviceApprovals
|
||||
)
|
||||
}
|
||||
value="on"
|
||||
checked={form.watch(
|
||||
|
||||
@@ -120,6 +120,7 @@ type DataTableProps<TData, TValue> = {
|
||||
// Row expansion props
|
||||
expandable?: boolean;
|
||||
renderExpandedRow?: (row: TData) => React.ReactNode;
|
||||
isExportDisabled?: boolean;
|
||||
};
|
||||
|
||||
export function LogDataTable<TData, TValue>({
|
||||
@@ -145,7 +146,8 @@ export function LogDataTable<TData, TValue>({
|
||||
isLoading = false,
|
||||
expandable = false,
|
||||
disabled = false,
|
||||
renderExpandedRow
|
||||
renderExpandedRow,
|
||||
isExportDisabled
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -403,7 +405,7 @@ export function LogDataTable<TData, TValue>({
|
||||
onClick={() =>
|
||||
!disabled && onExport()
|
||||
}
|
||||
disabled={isExporting || disabled}
|
||||
disabled={isExporting || disabled || isExportDisabled}
|
||||
>
|
||||
{isExporting ? (
|
||||
<Loader className="mr-2 size-4 animate-spin" />
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Separator } from "./ui/separator";
|
||||
import LoginPasswordForm from "./LoginPasswordForm";
|
||||
import IdpLoginButtons from "./private/IdpLoginButtons";
|
||||
import IdpLoginButtons from "./IdpLoginButtons";
|
||||
import { LookupUserResponse } from "@server/routers/auth/lookupUser";
|
||||
import UserProfileCard from "./UserProfileCard";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||
import { IdpDataTable } from "@app/components/private/OrgIdpDataTable";
|
||||
import { IdpDataTable } from "@app/components/OrgIdpDataTable";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
LoadLoginPageBrandingResponse,
|
||||
LoadLoginPageResponse
|
||||
} from "@server/routers/loginPage/types";
|
||||
import IdpLoginButtons from "@app/components/private/IdpLoginButtons";
|
||||
import IdpLoginButtons from "@app/components/IdpLoginButtons";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import { build } from "@server/build";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
@@ -6,6 +7,7 @@ import { ExternalLink, KeyRound, Sparkles } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { Tier } from "@server/types/Tiers";
|
||||
|
||||
const bannerClassName =
|
||||
"mb-6 border-primary/30 bg-linear-to-br from-primary/10 via-background to-background overflow-hidden";
|
||||
@@ -13,7 +15,11 @@ const bannerContentClassName = "py-3 px-4";
|
||||
const bannerRowClassName =
|
||||
"flex items-center gap-2.5 text-sm text-muted-foreground";
|
||||
|
||||
export function PaidFeaturesAlert() {
|
||||
type Props = {
|
||||
tiers: Tier[];
|
||||
};
|
||||
|
||||
export function PaidFeaturesAlert({ tiers }: Props) {
|
||||
const t = useTranslations();
|
||||
const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus();
|
||||
const { env } = useEnvContext();
|
||||
@@ -24,7 +30,7 @@ export function PaidFeaturesAlert() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{build === "saas" && !hasSaasSubscription ? (
|
||||
{build === "saas" && !hasSaasSubscription(tiers) ? (
|
||||
<Card className={bannerClassName}>
|
||||
<CardContent className={bannerContentClassName}>
|
||||
<div className={bannerRowClassName}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import { useTranslations } from "next-intl";
|
||||
import CertificateStatus from "@app/components/private/CertificateStatus";
|
||||
import CertificateStatus from "@app/components/CertificateStatus";
|
||||
import { toUnicode } from "punycode";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
|
||||
@@ -5,16 +5,12 @@ import DeleteRoleForm from "@app/components/DeleteRoleForm";
|
||||
import { RolesDataTable } from "@app/components/RolesDataTable";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { Role } from "@server/db";
|
||||
import { ArrowRight, ArrowUpDown, Link, MoreHorizontal } from "lucide-react";
|
||||
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -38,11 +34,6 @@ export default function UsersTable({ roles }: RolesTableProps) {
|
||||
|
||||
const [roleToRemove, setRoleToRemove] = useState<RoleRow | null>(null);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const { org } = useOrgContext();
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const t = useTranslations();
|
||||
const [isRefreshing, startTransition] = useTransition();
|
||||
|
||||
|
||||
@@ -10,22 +10,23 @@ export function usePaidStatus() {
|
||||
// Check if features are disabled due to licensing/subscription
|
||||
const hasEnterpriseLicense = build === "enterprise" && isUnlocked();
|
||||
const tierData = subscription?.getTier();
|
||||
const hasSaasSubscription = build === "saas" && tierData?.active;
|
||||
|
||||
function hasSaasSubscription(tiers: Tier[]): boolean {
|
||||
return (
|
||||
(build === "saas" &&
|
||||
tierData?.active &&
|
||||
tierData?.tier &&
|
||||
tiers.includes(tierData.tier)) ||
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
function isPaidUser(tiers: Tier[]): boolean {
|
||||
if (hasEnterpriseLicense) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
hasSaasSubscription &&
|
||||
tierData?.tier &&
|
||||
tiers.includes(tierData.tier)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return hasSaasSubscription(tiers);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user