mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-13 13:26:36 +00:00
Merge branch 'dev' into private-site-ha
This commit is contained in:
@@ -2113,9 +2113,11 @@
|
|||||||
"addDomainToEnableCustomAuthPages": "Users will be able to access the organization's login page and complete resource authentication using this domain.",
|
"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",
|
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
|
||||||
"domainPickerProvidedDomain": "Provided Domain",
|
"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",
|
"domainPickerVerified": "Verified",
|
||||||
"domainPickerUnverified": "Unverified",
|
"domainPickerUnverified": "Unverified",
|
||||||
|
"domainPickerManual": "Manual",
|
||||||
"domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.",
|
"domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.",
|
||||||
"domainPickerError": "Error",
|
"domainPickerError": "Error",
|
||||||
"domainPickerErrorLoadDomains": "Failed to load organization domains",
|
"domainPickerErrorLoadDomains": "Failed to load organization domains",
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export enum TierFeature {
|
|||||||
SshPam = "sshPam",
|
SshPam = "sshPam",
|
||||||
FullRbac = "fullRbac",
|
FullRbac = "fullRbac",
|
||||||
SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed
|
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<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
@@ -56,5 +57,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"],
|
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"],
|
||||||
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"],
|
[TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"],
|
||||||
[TierFeature.SIEM]: ["enterprise"]
|
[TierFeature.SIEM]: ["enterprise"],
|
||||||
|
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,11 +22,15 @@ import { OpenAPITags, registry } from "@server/openApi";
|
|||||||
import { db, domainNamespaces, resources } from "@server/db";
|
import { db, domainNamespaces, resources } from "@server/db";
|
||||||
import { inArray } from "drizzle-orm";
|
import { inArray } from "drizzle-orm";
|
||||||
import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types";
|
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 paramsSchema = z.strictObject({});
|
||||||
|
|
||||||
const querySchema = 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({
|
registry.registerPath({
|
||||||
@@ -58,6 +62,23 @@ export async function checkDomainNamespaceAvailability(
|
|||||||
}
|
}
|
||||||
const { subdomain } = parsedQuery.data;
|
const { subdomain } = parsedQuery.data;
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// build == "saas" &&
|
||||||
|
// !isSubscribed(orgId!, tierMatrix.domainNamespaces)
|
||||||
|
// ) {
|
||||||
|
// // return not available
|
||||||
|
// return response<CheckDomainAvailabilityResponse>(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);
|
const namespaces = await db.select().from(domainNamespaces);
|
||||||
let possibleDomains = namespaces.map((ns) => {
|
let possibleDomains = namespaces.map((ns) => {
|
||||||
const desired = `${subdomain}.${ns.domainNamespaceId}`;
|
const desired = `${subdomain}.${ns.domainNamespaceId}`;
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import { eq, sql } from "drizzle-orm";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
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({});
|
const paramsSchema = z.strictObject({});
|
||||||
|
|
||||||
@@ -37,7 +40,8 @@ const querySchema = z.strictObject({
|
|||||||
.optional()
|
.optional()
|
||||||
.default("0")
|
.default("0")
|
||||||
.transform(Number)
|
.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) {
|
async function query(limit: number, offset: number) {
|
||||||
@@ -99,6 +103,26 @@ export async function listDomainNamespaces(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// build == "saas" &&
|
||||||
|
// !isSubscribed(orgId!, tierMatrix.domainNamespaces)
|
||||||
|
// ) {
|
||||||
|
// return response<ListDomainNamespacesResponse>(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 domainNamespacesList = await query(limit, offset);
|
||||||
|
|
||||||
const [{ count }] = await db
|
const [{ count }] = await db
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, loginPage } from "@server/db";
|
import { db, domainNamespaces, loginPage } from "@server/db";
|
||||||
import {
|
import {
|
||||||
domains,
|
domains,
|
||||||
orgDomains,
|
orgDomains,
|
||||||
@@ -24,6 +24,8 @@ import { build } from "@server/build";
|
|||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
import { getUniqueResourceName } from "@server/db/names";
|
import { getUniqueResourceName } from "@server/db/names";
|
||||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||||
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
|
||||||
const createResourceParamsSchema = z.strictObject({
|
const createResourceParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -112,7 +114,10 @@ export async function createResource(
|
|||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
|
if (
|
||||||
|
req.user &&
|
||||||
|
(!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)
|
||||||
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||||
);
|
);
|
||||||
@@ -193,6 +198,29 @@ async function createHttpResource(
|
|||||||
const subdomain = parsedBody.data.subdomain;
|
const subdomain = parsedBody.data.subdomain;
|
||||||
const stickySession = parsedBody.data.stickySession;
|
const stickySession = parsedBody.data.stickySession;
|
||||||
|
|
||||||
|
if (build == "saas" && !isSubscribed(orgId!, tierMatrix.domainNamespaces)) {
|
||||||
|
// grandfather in existing users
|
||||||
|
const lastAllowedDate = new Date("2026-04-12");
|
||||||
|
const userCreatedDate = new Date(req.user?.dateCreated || new Date());
|
||||||
|
if (userCreatedDate > lastAllowedDate) {
|
||||||
|
// 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
|
// Validate domain and construct full domain
|
||||||
const domainResult = await validateAndConstructDomain(
|
const domainResult = await validateAndConstructDomain(
|
||||||
domainId,
|
domainId,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, loginPage } from "@server/db";
|
import { db, domainNamespaces, loginPage } from "@server/db";
|
||||||
import {
|
import {
|
||||||
domains,
|
domains,
|
||||||
Org,
|
Org,
|
||||||
@@ -25,6 +25,7 @@ import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
|
|
||||||
const updateResourceParamsSchema = z.strictObject({
|
const updateResourceParamsSchema = z.strictObject({
|
||||||
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
@@ -120,7 +121,9 @@ const updateHttpResourceBodySchema = z
|
|||||||
if (data.headers) {
|
if (data.headers) {
|
||||||
// HTTP header values must be visible ASCII or horizontal whitespace, no control chars (RFC 7230)
|
// HTTP header values must be visible ASCII or horizontal whitespace, no control chars (RFC 7230)
|
||||||
const validHeaderValue = /^[\t\x20-\x7E]*$/;
|
const validHeaderValue = /^[\t\x20-\x7E]*$/;
|
||||||
return data.headers.every((h) => validHeaderValue.test(h.value));
|
return data.headers.every((h) =>
|
||||||
|
validHeaderValue.test(h.value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -318,6 +321,34 @@ async function updateHttpResource(
|
|||||||
if (updateData.domainId) {
|
if (updateData.domainId) {
|
||||||
const domainId = updateData.domainId;
|
const domainId = updateData.domainId;
|
||||||
|
|
||||||
|
if (
|
||||||
|
build == "saas" &&
|
||||||
|
!isSubscribed(resource.orgId, tierMatrix.domainNamespaces)
|
||||||
|
) {
|
||||||
|
// grandfather in existing users
|
||||||
|
const lastAllowedDate = new Date("2026-04-12");
|
||||||
|
const userCreatedDate = new Date(
|
||||||
|
req.user?.dateCreated || new Date()
|
||||||
|
);
|
||||||
|
if (userCreatedDate > lastAllowedDate) {
|
||||||
|
// 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
|
// Validate domain and construct full domain
|
||||||
const domainResult = await validateAndConstructDomain(
|
const domainResult = await validateAndConstructDomain(
|
||||||
domainId,
|
domainId,
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { orgs, roles, userInviteRoles, userInvites, userOrgs, users } from "@server/db";
|
import {
|
||||||
|
orgs,
|
||||||
|
roles,
|
||||||
|
userInviteRoles,
|
||||||
|
userInvites,
|
||||||
|
userOrgs,
|
||||||
|
users
|
||||||
|
} from "@server/db";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -37,8 +44,7 @@ const inviteUserBodySchema = z
|
|||||||
regenerate: z.boolean().optional()
|
regenerate: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(d) =>
|
(d) => (d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
|
||||||
(d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
|
|
||||||
{ message: "roleIds or roleId is required", path: ["roleIds"] }
|
{ message: "roleIds or roleId is required", path: ["roleIds"] }
|
||||||
)
|
)
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
@@ -265,7 +271,7 @@ export async function inviteUser(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
|
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`;
|
||||||
|
|
||||||
if (doEmail) {
|
if (doEmail) {
|
||||||
await sendEmail(
|
await sendEmail(
|
||||||
@@ -314,12 +320,12 @@ export async function inviteUser(
|
|||||||
expiresAt,
|
expiresAt,
|
||||||
tokenHash
|
tokenHash
|
||||||
});
|
});
|
||||||
await trx.insert(userInviteRoles).values(
|
await trx
|
||||||
uniqueRoleIds.map((roleId) => ({ inviteId, roleId }))
|
.insert(userInviteRoles)
|
||||||
);
|
.values(uniqueRoleIds.map((roleId) => ({ inviteId, roleId })));
|
||||||
});
|
});
|
||||||
|
|
||||||
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
|
const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`;
|
||||||
|
|
||||||
if (doEmail) {
|
if (doEmail) {
|
||||||
await sendEmail(
|
await sendEmail(
|
||||||
|
|||||||
@@ -678,6 +678,7 @@ function ProxyResourceTargetsForm({
|
|||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getRowId: (row) => String(row.targetId),
|
||||||
state: {
|
state: {
|
||||||
pagination: {
|
pagination: {
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
|
|||||||
@@ -999,6 +999,7 @@ export default function Page() {
|
|||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getRowId: (row) => String(row.targetId),
|
||||||
state: {
|
state: {
|
||||||
pagination: {
|
pagination: {
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ export default function DeviceLoginForm({
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<InputOTP
|
<InputOTP
|
||||||
maxLength={9}
|
maxLength={9}
|
||||||
|
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value
|
value={field.value
|
||||||
.replace(/-/g, "")
|
.replace(/-/g, "")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -40,9 +41,12 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
|
KeyRound,
|
||||||
Zap
|
Zap
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { usePaidStatus } from "@/hooks/usePaidStatus";
|
||||||
|
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { toUnicode } from "punycode";
|
import { toUnicode } from "punycode";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
@@ -95,6 +99,7 @@ export default function DomainPicker({
|
|||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { hasSaasSubscription } = usePaidStatus();
|
||||||
|
|
||||||
const { data = [], isLoading: loadingDomains } = useQuery(
|
const { data = [], isLoading: loadingDomains } = useQuery(
|
||||||
orgQueries.domains({ orgId })
|
orgQueries.domains({ orgId })
|
||||||
@@ -509,9 +514,11 @@ export default function DomainPicker({
|
|||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{selectedBaseDomain.domain}
|
{selectedBaseDomain.domain}
|
||||||
</span>
|
</span>
|
||||||
{selectedBaseDomain.verified && (
|
{selectedBaseDomain.verified &&
|
||||||
<CheckCircle2 className="h-3 w-3 text-green-500 shrink-0" />
|
selectedBaseDomain.domainType !==
|
||||||
)}
|
"wildcard" && (
|
||||||
|
<CheckCircle2 className="h-3 w-3 text-green-500 shrink-0" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
t("domainPickerSelectBaseDomain")
|
t("domainPickerSelectBaseDomain")
|
||||||
@@ -574,14 +581,23 @@ export default function DomainPicker({
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{orgDomain.type.toUpperCase()}{" "}
|
{orgDomain.type ===
|
||||||
•{" "}
|
"wildcard"
|
||||||
{orgDomain.verified
|
|
||||||
? t(
|
? t(
|
||||||
"domainPickerVerified"
|
"domainPickerManual"
|
||||||
)
|
)
|
||||||
: t(
|
: (
|
||||||
"domainPickerUnverified"
|
<>
|
||||||
|
{orgDomain.type.toUpperCase()}{" "}
|
||||||
|
•{" "}
|
||||||
|
{orgDomain.verified
|
||||||
|
? t(
|
||||||
|
"domainPickerVerified"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"domainPickerUnverified"
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -680,6 +696,23 @@ export default function DomainPicker({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{build === "saas" &&
|
||||||
|
!hasSaasSubscription(
|
||||||
|
tierMatrix[TierFeature.DomainNamespaces]
|
||||||
|
) &&
|
||||||
|
!hideFreeDomain && (
|
||||||
|
<Card className="mt-3 border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden">
|
||||||
|
<CardContent className="py-3 px-4">
|
||||||
|
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
||||||
|
<KeyRound className="size-4 shrink-0 text-black-500" />
|
||||||
|
<span>
|
||||||
|
{t("domainPickerFreeDomainsPaidFeature")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/*showProvidedDomainSearch && build === "saas" && (
|
{/*showProvidedDomainSearch && build === "saas" && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ export default function InviteStatusCard({
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [type, setType] = useState<
|
const [type, setType] = useState<
|
||||||
"rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in" | "user_limit_exceeded"
|
| "rejected"
|
||||||
|
| "wrong_user"
|
||||||
|
| "user_does_not_exist"
|
||||||
|
| "not_logged_in"
|
||||||
|
| "user_limit_exceeded"
|
||||||
>("rejected");
|
>("rejected");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -90,12 +94,12 @@ export default function InviteStatusCard({
|
|||||||
|
|
||||||
if (!user && type === "user_does_not_exist") {
|
if (!user && type === "user_does_not_exist") {
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
|
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}`
|
||||||
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
|
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
} else if (!user && type === "not_logged_in") {
|
} else if (!user && type === "not_logged_in") {
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
|
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
|
||||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +113,7 @@ export default function InviteStatusCard({
|
|||||||
async function goToLogin() {
|
async function goToLogin() {
|
||||||
await api.post("/auth/logout", {});
|
await api.post("/auth/logout", {});
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
|
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
|
||||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
}
|
}
|
||||||
@@ -117,7 +121,7 @@ export default function InviteStatusCard({
|
|||||||
async function goToSignup() {
|
async function goToSignup() {
|
||||||
await api.post("/auth/logout", {});
|
await api.post("/auth/logout", {});
|
||||||
const redirectUrl = email
|
const redirectUrl = email
|
||||||
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
|
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}`
|
||||||
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
|
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
|
||||||
router.push(redirectUrl);
|
router.push(redirectUrl);
|
||||||
}
|
}
|
||||||
@@ -157,7 +161,9 @@ export default function InviteStatusCard({
|
|||||||
Cannot Accept Invite
|
Cannot Accept Invite
|
||||||
</p>
|
</p>
|
||||||
<p className="text-center text-sm">
|
<p className="text-center text-sm">
|
||||||
This organization has reached its user limit. Please contact the organization administrator to upgrade their plan before accepting this invite.
|
This organization has reached its user limit. Please
|
||||||
|
contact the organization administrator to upgrade their
|
||||||
|
plan before accepting this invite.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const countryClass = cn(
|
|||||||
|
|
||||||
const highlightedCountryClass = cn(
|
const highlightedCountryClass = cn(
|
||||||
sharedCountryClass,
|
sharedCountryClass,
|
||||||
"stroke-2",
|
"stroke-[3]",
|
||||||
"fill-[#f4f4f5]",
|
"fill-[#f4f4f5]",
|
||||||
"stroke-[#f36117]",
|
"stroke-[#f36117]",
|
||||||
"dark:fill-[#3f3f46]"
|
"dark:fill-[#3f3f46]"
|
||||||
@@ -194,11 +194,20 @@ function drawInteractiveCountries(
|
|||||||
const path = setupProjetionPath();
|
const path = setupProjetionPath();
|
||||||
const data = parseWorldTopoJsonToGeoJsonFeatures();
|
const data = parseWorldTopoJsonToGeoJsonFeatures();
|
||||||
const svg = d3.select(element);
|
const svg = d3.select(element);
|
||||||
|
const countriesLayer = svg.append("g");
|
||||||
|
const hoverLayer = svg.append("g").style("pointer-events", "none");
|
||||||
|
const hoverPath = hoverLayer
|
||||||
|
.append("path")
|
||||||
|
.datum(null)
|
||||||
|
.attr("class", highlightedCountryClass)
|
||||||
|
.style("display", "none");
|
||||||
|
|
||||||
svg.selectAll("path")
|
countriesLayer
|
||||||
|
.selectAll("path")
|
||||||
.data(data)
|
.data(data)
|
||||||
.enter()
|
.enter()
|
||||||
.append("path")
|
.append("path")
|
||||||
|
.attr("data-country-path", "true")
|
||||||
.attr("class", countryClass)
|
.attr("class", countryClass)
|
||||||
.attr("d", path as never)
|
.attr("d", path as never)
|
||||||
|
|
||||||
@@ -209,9 +218,10 @@ function drawInteractiveCountries(
|
|||||||
y,
|
y,
|
||||||
hoveredCountryAlpha3Code: country.properties.a3
|
hoveredCountryAlpha3Code: country.properties.a3
|
||||||
});
|
});
|
||||||
// brings country to front
|
hoverPath
|
||||||
this.parentNode?.appendChild(this);
|
.datum(country)
|
||||||
d3.select(this).attr("class", highlightedCountryClass);
|
.attr("d", path(country) as string)
|
||||||
|
.style("display", null);
|
||||||
})
|
})
|
||||||
|
|
||||||
.on("mousemove", function (event) {
|
.on("mousemove", function (event) {
|
||||||
@@ -221,7 +231,7 @@ function drawInteractiveCountries(
|
|||||||
|
|
||||||
.on("mouseout", function () {
|
.on("mouseout", function () {
|
||||||
setTooltip({ x: 0, y: 0, hoveredCountryAlpha3Code: null });
|
setTooltip({ x: 0, y: 0, hoveredCountryAlpha3Code: null });
|
||||||
d3.select(this).attr("class", countryClass);
|
hoverPath.style("display", "none");
|
||||||
});
|
});
|
||||||
|
|
||||||
return svg;
|
return svg;
|
||||||
@@ -257,7 +267,7 @@ function colorInCountriesWithValues(
|
|||||||
const svg = d3.select(element);
|
const svg = d3.select(element);
|
||||||
|
|
||||||
return svg
|
return svg
|
||||||
.selectAll("path")
|
.selectAll('path[data-country-path="true"]')
|
||||||
.style("fill", (countryPath) => {
|
.style("fill", (countryPath) => {
|
||||||
const country = getCountryByCountryPath(countryPath);
|
const country = getCountryByCountryPath(countryPath);
|
||||||
if (!country?.count) {
|
if (!country?.count) {
|
||||||
|
|||||||
Reference in New Issue
Block a user