diff --git a/messages/en-US.json b/messages/en-US.json index 9fa4e730d..5c86aabec 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2113,7 +2113,8 @@ "addDomainToEnableCustomAuthPages": "Users will be able to access the organization's login page and complete resource authentication using this domain.", "selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page", "domainPickerProvidedDomain": "Provided Domain", - "domainPickerFreeProvidedDomain": "Free Provided Domain", + "domainPickerFreeProvidedDomain": "Provided Domain", + "domainPickerFreeDomainsPaidFeature": "Provided domains are a paid feature. Subscribe to get a domain included with your plan — no need to bring your own.", "domainPickerVerified": "Verified", "domainPickerUnverified": "Unverified", "domainPickerManual": "Manual", diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index c76dcd95b..0756ea665 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -19,7 +19,8 @@ export enum TierFeature { SshPam = "sshPam", FullRbac = "fullRbac", SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed - SIEM = "siem" // handle downgrade by disabling SIEM integrations + SIEM = "siem", // handle downgrade by disabling SIEM integrations + DomainNamespaces = "domainNamespaces" // handle downgrade by removing custom domain namespaces } export const tierMatrix: Record = { @@ -56,5 +57,6 @@ export const tierMatrix: Record = { [TierFeature.SshPam]: ["tier1", "tier3", "enterprise"], [TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"], - [TierFeature.SIEM]: ["enterprise"] + [TierFeature.SIEM]: ["enterprise"], + [TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"] }; diff --git a/server/private/routers/domain/checkDomainNamespaceAvailability.ts b/server/private/routers/domain/checkDomainNamespaceAvailability.ts index db9a4b46a..0bb7f8704 100644 --- a/server/private/routers/domain/checkDomainNamespaceAvailability.ts +++ b/server/private/routers/domain/checkDomainNamespaceAvailability.ts @@ -22,11 +22,15 @@ import { OpenAPITags, registry } from "@server/openApi"; import { db, domainNamespaces, resources } from "@server/db"; import { inArray } from "drizzle-orm"; import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; +import { build } from "@server/build"; +import { isSubscribed } from "#private/lib/isSubscribed"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; const paramsSchema = z.strictObject({}); const querySchema = z.strictObject({ - subdomain: z.string() + subdomain: z.string(), + // orgId: build === "saas" ? z.string() : z.string().optional() // Required for saas, optional otherwise }); registry.registerPath({ @@ -58,6 +62,23 @@ export async function checkDomainNamespaceAvailability( } const { subdomain } = parsedQuery.data; + // if ( + // build == "saas" && + // !isSubscribed(orgId!, tierMatrix.domainNamespaces) + // ) { + // // return not available + // return response(res, { + // data: { + // available: false, + // options: [] + // }, + // success: true, + // error: false, + // message: "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature.", + // status: HttpCode.OK + // }); + // } + const namespaces = await db.select().from(domainNamespaces); let possibleDomains = namespaces.map((ns) => { const desired = `${subdomain}.${ns.domainNamespaceId}`; diff --git a/server/private/routers/domain/listDomainNamespaces.ts b/server/private/routers/domain/listDomainNamespaces.ts index 180613a85..5bbd25b1a 100644 --- a/server/private/routers/domain/listDomainNamespaces.ts +++ b/server/private/routers/domain/listDomainNamespaces.ts @@ -22,6 +22,9 @@ import { eq, sql } from "drizzle-orm"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import { isSubscribed } from "#private/lib/isSubscribed"; +import { build } from "@server/build"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; const paramsSchema = z.strictObject({}); @@ -37,7 +40,8 @@ const querySchema = z.strictObject({ .optional() .default("0") .transform(Number) - .pipe(z.int().nonnegative()) + .pipe(z.int().nonnegative()), + // orgId: build === "saas" ? z.string() : z.string().optional() // Required for saas, optional otherwise }); async function query(limit: number, offset: number) { @@ -99,6 +103,26 @@ export async function listDomainNamespaces( ); } + // if ( + // build == "saas" && + // !isSubscribed(orgId!, tierMatrix.domainNamespaces) + // ) { + // return response(res, { + // data: { + // domainNamespaces: [], + // pagination: { + // total: 0, + // limit, + // offset + // } + // }, + // success: true, + // error: false, + // message: "No namespaces found. Your current subscription does not support custom domain namespaces. Please upgrade to access this feature.", + // status: HttpCode.OK + // }); + // } + const domainNamespacesList = await query(limit, offset); const [{ count }] = await db diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 6cff4d23a..e94a5fc10 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, loginPage } from "@server/db"; +import { db, domainNamespaces, loginPage } from "@server/db"; import { domains, orgDomains, @@ -24,6 +24,8 @@ import { build } from "@server/build"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { getUniqueResourceName } from "@server/db/names"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; +import { isSubscribed } from "#dynamic/lib/isSubscribed"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; const createResourceParamsSchema = z.strictObject({ orgId: z.string() @@ -193,6 +195,27 @@ async function createHttpResource( const subdomain = parsedBody.data.subdomain; const stickySession = parsedBody.data.stickySession; + if ( + build == "saas" && + !isSubscribed(orgId!, tierMatrix.domainNamespaces) + ) { + // check if this domain id is a namespace domain and if so, reject + const domain = await db + .select() + .from(domainNamespaces) + .where(eq(domainNamespaces.domainId, domainId)) + .limit(1); + + if (domain.length > 0) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature." + ) + ); + } + } + // Validate domain and construct full domain const domainResult = await validateAndConstructDomain( domainId, diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 01f3e79ff..8a2df18c3 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, loginPage } from "@server/db"; +import { db, domainNamespaces, loginPage } from "@server/db"; import { domains, Org, @@ -25,6 +25,7 @@ import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { build } from "@server/build"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { isSubscribed } from "#dynamic/lib/isSubscribed"; const updateResourceParamsSchema = z.strictObject({ resourceId: z.string().transform(Number).pipe(z.int().positive()) @@ -318,6 +319,27 @@ async function updateHttpResource( if (updateData.domainId) { const domainId = updateData.domainId; + if ( + build == "saas" && + !isSubscribed(resource.orgId, tierMatrix.domainNamespaces) + ) { + // check if this domain id is a namespace domain and if so, reject + const domain = await db + .select() + .from(domainNamespaces) + .where(eq(domainNamespaces.domainId, domainId)) + .limit(1); + + if (domain.length > 0) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature." + ) + ); + } + } + // Validate domain and construct full domain const domainResult = await validateAndConstructDomain( domainId, @@ -366,7 +388,7 @@ async function updateHttpResource( ); } } - + if (build != "oss") { const existingLoginPages = await db .select() diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index afb273b5c..e1ec1062e 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -2,6 +2,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; import { Command, CommandEmpty, @@ -40,9 +41,12 @@ import { Check, CheckCircle2, ChevronsUpDown, + KeyRound, Zap } from "lucide-react"; import { useTranslations } from "next-intl"; +import { usePaidStatus } from "@/hooks/usePaidStatus"; +import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { toUnicode } from "punycode"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -95,6 +99,7 @@ export default function DomainPicker({ const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); + const { hasSaasSubscription } = usePaidStatus(); const { data = [], isLoading: loadingDomains } = useQuery( orgQueries.domains({ orgId }) @@ -691,6 +696,23 @@ export default function DomainPicker({ + {build === "saas" && + !hasSaasSubscription( + tierMatrix[TierFeature.DomainNamespaces] + ) && + !hideFreeDomain && ( + + +
+ + + {t("domainPickerFreeDomainsPaidFeature")} + +
+
+
+ )} + {/*showProvidedDomainSearch && build === "saas" && (