mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-14 05:46:38 +00:00
Compare commits
6 Commits
1.17.1-s.0
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74b3b283f7 | ||
|
|
03d95874e6 | ||
|
|
5d51af4330 | ||
|
|
93998f9fd5 | ||
|
|
c554e69514 | ||
|
|
a6e10e55cc |
BIN
config/db/db.sqlite-journal
Normal file
BIN
config/db/db.sqlite-journal
Normal file
Binary file not shown.
@@ -591,7 +591,7 @@ export function generateSubnetProxyTargetV2(
|
||||
pubKey: string | null;
|
||||
subnet: string | null;
|
||||
}[]
|
||||
): SubnetProxyTargetV2 | undefined {
|
||||
): SubnetProxyTargetV2[] | undefined {
|
||||
if (clients.length === 0) {
|
||||
logger.debug(
|
||||
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
|
||||
@@ -599,7 +599,7 @@ export function generateSubnetProxyTargetV2(
|
||||
return;
|
||||
}
|
||||
|
||||
let target: SubnetProxyTargetV2 | null = null;
|
||||
let targets: SubnetProxyTargetV2[] = [];
|
||||
|
||||
const portRange = [
|
||||
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
|
||||
@@ -614,40 +614,41 @@ export function generateSubnetProxyTargetV2(
|
||||
if (ipSchema.safeParse(destination).success) {
|
||||
destination = `${destination}/32`;
|
||||
|
||||
target = {
|
||||
targets.push({
|
||||
sourcePrefixes: [],
|
||||
destPrefix: destination,
|
||||
portRange,
|
||||
disableIcmp,
|
||||
resourceId: siteResource.siteResourceId,
|
||||
};
|
||||
resourceId: siteResource.siteResourceId
|
||||
});
|
||||
}
|
||||
|
||||
if (siteResource.alias && siteResource.aliasAddress) {
|
||||
// also push a match for the alias address
|
||||
target = {
|
||||
targets.push({
|
||||
sourcePrefixes: [],
|
||||
destPrefix: `${siteResource.aliasAddress}/32`,
|
||||
rewriteTo: destination,
|
||||
portRange,
|
||||
disableIcmp,
|
||||
resourceId: siteResource.siteResourceId,
|
||||
};
|
||||
resourceId: siteResource.siteResourceId
|
||||
});
|
||||
}
|
||||
} else if (siteResource.mode == "cidr") {
|
||||
target = {
|
||||
targets.push({
|
||||
sourcePrefixes: [],
|
||||
destPrefix: siteResource.destination,
|
||||
portRange,
|
||||
disableIcmp,
|
||||
resourceId: siteResource.siteResourceId,
|
||||
};
|
||||
resourceId: siteResource.siteResourceId
|
||||
});
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
if (targets.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const target of targets) {
|
||||
for (const clientSite of clients) {
|
||||
if (!clientSite.subnet) {
|
||||
logger.debug(
|
||||
@@ -661,25 +662,25 @@ export function generateSubnetProxyTargetV2(
|
||||
// add client prefix to source prefixes
|
||||
target.sourcePrefixes.push(clientPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
// print a nice representation of the targets
|
||||
// logger.debug(
|
||||
// `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}`
|
||||
// );
|
||||
|
||||
return target;
|
||||
return targets;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1)
|
||||
* by expanding each source prefix into its own target entry.
|
||||
* @param targetV2 - The v2 target to convert
|
||||
* @returns Array of v1 SubnetProxyTarget objects
|
||||
*/
|
||||
export function convertSubnetProxyTargetsV2ToV1(
|
||||
export function convertSubnetProxyTargetsV2ToV1(
|
||||
targetsV2: SubnetProxyTargetV2[]
|
||||
): SubnetProxyTarget[] {
|
||||
): SubnetProxyTarget[] {
|
||||
return targetsV2.flatMap((targetV2) =>
|
||||
targetV2.sourcePrefixes.map((sourcePrefix) => ({
|
||||
sourcePrefix,
|
||||
@@ -695,8 +696,7 @@ export function generateSubnetProxyTargetV2(
|
||||
})
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Custom schema for validating port range strings
|
||||
// Format: "80,443,8000-9000" or "*" for all ports, or empty string
|
||||
|
||||
@@ -661,16 +661,16 @@ async function handleSubnetProxyTargetUpdates(
|
||||
);
|
||||
|
||||
if (addedClients.length > 0) {
|
||||
const targetToAdd = generateSubnetProxyTargetV2(
|
||||
const targetsToAdd = generateSubnetProxyTargetV2(
|
||||
siteResource,
|
||||
addedClients
|
||||
);
|
||||
|
||||
if (targetToAdd) {
|
||||
if (targetsToAdd) {
|
||||
proxyJobs.push(
|
||||
addSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
[targetToAdd],
|
||||
targetsToAdd,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
@@ -698,16 +698,16 @@ async function handleSubnetProxyTargetUpdates(
|
||||
);
|
||||
|
||||
if (removedClients.length > 0) {
|
||||
const targetToRemove = generateSubnetProxyTargetV2(
|
||||
const targetsToRemove = generateSubnetProxyTargetV2(
|
||||
siteResource,
|
||||
removedClients
|
||||
);
|
||||
|
||||
if (targetToRemove) {
|
||||
if (targetsToRemove) {
|
||||
proxyJobs.push(
|
||||
removeSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
[targetToRemove],
|
||||
targetsToRemove,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
@@ -1164,7 +1164,7 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
|
||||
for (const resource of resources) {
|
||||
const target = generateSubnetProxyTargetV2(resource, [
|
||||
const targets = generateSubnetProxyTargetV2(resource, [
|
||||
{
|
||||
clientId: client.clientId,
|
||||
pubKey: client.pubKey,
|
||||
@@ -1172,11 +1172,11 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
]);
|
||||
|
||||
if (target) {
|
||||
if (targets) {
|
||||
proxyJobs.push(
|
||||
addSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
[target],
|
||||
targets,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
@@ -1241,7 +1241,7 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
|
||||
for (const resource of resources) {
|
||||
const target = generateSubnetProxyTargetV2(resource, [
|
||||
const targets = generateSubnetProxyTargetV2(resource, [
|
||||
{
|
||||
clientId: client.clientId,
|
||||
pubKey: client.pubKey,
|
||||
@@ -1249,11 +1249,11 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
]);
|
||||
|
||||
if (target) {
|
||||
if (targets) {
|
||||
proxyJobs.push(
|
||||
removeSubnetProxyTargets(
|
||||
newt.newtId,
|
||||
[target],
|
||||
targets,
|
||||
newt.version
|
||||
)
|
||||
);
|
||||
|
||||
@@ -29,65 +29,9 @@ import {
|
||||
} from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import NodeCache from "node-cache";
|
||||
import semver from "semver";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
|
||||
|
||||
async function getLatestOlmVersion(): Promise<string | null> {
|
||||
try {
|
||||
const cachedVersion = olmVersionCache.get<string>("latestOlmVersion");
|
||||
if (cachedVersion) {
|
||||
return cachedVersion;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/fosrl/olm/tags",
|
||||
{
|
||||
signal: controller.signal
|
||||
}
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn(
|
||||
`Failed to fetch latest Olm version from GitHub: ${response.status} ${response.statusText}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let tags = await response.json();
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
logger.warn("No tags found for Olm repository");
|
||||
return null;
|
||||
}
|
||||
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||
const latestVersion = tags[0].name;
|
||||
|
||||
olmVersionCache.set("latestOlmVersion", latestVersion, 3600);
|
||||
|
||||
return latestVersion;
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
logger.warn("Request to fetch latest Olm version timed out (1.5s)");
|
||||
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
|
||||
logger.warn("Connection timeout while fetching latest Olm version");
|
||||
} else {
|
||||
logger.warn(
|
||||
"Error fetching latest Olm version:",
|
||||
error.message || error
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const listClientsParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
@@ -413,44 +357,45 @@ export async function listClients(
|
||||
};
|
||||
});
|
||||
|
||||
const latestOlVersionPromise = getLatestOlmVersion();
|
||||
// REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW
|
||||
// const latestOlmVersionPromise = getLatestOlmVersion();
|
||||
|
||||
const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map(
|
||||
(client) => {
|
||||
const OlmWithUpdate: OlmWithUpdateAvailable = { ...client };
|
||||
// Initially set to false, will be updated if version check succeeds
|
||||
OlmWithUpdate.olmUpdateAvailable = false;
|
||||
return OlmWithUpdate;
|
||||
}
|
||||
);
|
||||
// const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map(
|
||||
// (client) => {
|
||||
// const OlmWithUpdate: OlmWithUpdateAvailable = { ...client };
|
||||
// // Initially set to false, will be updated if version check succeeds
|
||||
// OlmWithUpdate.olmUpdateAvailable = false;
|
||||
// return OlmWithUpdate;
|
||||
// }
|
||||
// );
|
||||
|
||||
// Try to get the latest version, but don't block if it fails
|
||||
try {
|
||||
const latestOlVersion = await latestOlVersionPromise;
|
||||
// try {
|
||||
// const latestOlmVersion = await latestOlVersionPromise;
|
||||
|
||||
if (latestOlVersion) {
|
||||
olmsWithUpdates.forEach((client) => {
|
||||
try {
|
||||
client.olmUpdateAvailable = semver.lt(
|
||||
client.olmVersion ? client.olmVersion : "",
|
||||
latestOlVersion
|
||||
);
|
||||
} catch (error) {
|
||||
client.olmUpdateAvailable = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error but don't let it block the response
|
||||
logger.warn(
|
||||
"Failed to check for OLM updates, continuing without update info:",
|
||||
error
|
||||
);
|
||||
}
|
||||
// if (latestOlVersion) {
|
||||
// olmsWithUpdates.forEach((client) => {
|
||||
// try {
|
||||
// client.olmUpdateAvailable = semver.lt(
|
||||
// client.olmVersion ? client.olmVersion : "",
|
||||
// latestOlVersion
|
||||
// );
|
||||
// } catch (error) {
|
||||
// client.olmUpdateAvailable = false;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// // Log the error but don't let it block the response
|
||||
// logger.warn(
|
||||
// "Failed to check for OLM updates, continuing without update info:",
|
||||
// error
|
||||
// );
|
||||
// }
|
||||
|
||||
return response<ListClientsResponse>(res, {
|
||||
data: {
|
||||
clients: olmsWithUpdates,
|
||||
clients: clientsWithSites,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
page,
|
||||
|
||||
@@ -30,65 +30,10 @@ import {
|
||||
} from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import NodeCache from "node-cache";
|
||||
import semver from "semver";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
|
||||
|
||||
async function getLatestOlmVersion(): Promise<string | null> {
|
||||
try {
|
||||
const cachedVersion = olmVersionCache.get<string>("latestOlmVersion");
|
||||
if (cachedVersion) {
|
||||
return cachedVersion;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/fosrl/olm/tags",
|
||||
{
|
||||
signal: controller.signal
|
||||
}
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn(
|
||||
`Failed to fetch latest Olm version from GitHub: ${response.status} ${response.statusText}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let tags = await response.json();
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
logger.warn("No tags found for Olm repository");
|
||||
return null;
|
||||
}
|
||||
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||
const latestVersion = tags[0].name;
|
||||
|
||||
olmVersionCache.set("latestOlmVersion", latestVersion, 3600);
|
||||
|
||||
return latestVersion;
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
logger.warn("Request to fetch latest Olm version timed out (1.5s)");
|
||||
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
|
||||
logger.warn("Connection timeout while fetching latest Olm version");
|
||||
} else {
|
||||
logger.warn(
|
||||
"Error fetching latest Olm version:",
|
||||
error.message || error
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const listUserDevicesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
@@ -453,29 +398,30 @@ export async function listUserDevices(
|
||||
}
|
||||
);
|
||||
|
||||
// Try to get the latest version, but don't block if it fails
|
||||
try {
|
||||
const latestOlmVersion = await getLatestOlmVersion();
|
||||
// REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW
|
||||
// // Try to get the latest version, but don't block if it fails
|
||||
// try {
|
||||
// const latestOlmVersion = await getLatestOlmVersion();
|
||||
|
||||
if (latestOlmVersion) {
|
||||
olmsWithUpdates.forEach((client) => {
|
||||
try {
|
||||
client.olmUpdateAvailable = semver.lt(
|
||||
client.olmVersion ? client.olmVersion : "",
|
||||
latestOlmVersion
|
||||
);
|
||||
} catch (error) {
|
||||
client.olmUpdateAvailable = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error but don't let it block the response
|
||||
logger.warn(
|
||||
"Failed to check for OLM updates, continuing without update info:",
|
||||
error
|
||||
);
|
||||
}
|
||||
// if (latestOlmVersion) {
|
||||
// olmsWithUpdates.forEach((client) => {
|
||||
// try {
|
||||
// client.olmUpdateAvailable = semver.lt(
|
||||
// client.olmVersion ? client.olmVersion : "",
|
||||
// latestOlmVersion
|
||||
// );
|
||||
// } catch (error) {
|
||||
// client.olmUpdateAvailable = false;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// // Log the error but don't let it block the response
|
||||
// logger.warn(
|
||||
// "Failed to check for OLM updates, continuing without update info:",
|
||||
// error
|
||||
// );
|
||||
// }
|
||||
|
||||
return response<ListUserDevicesResponse>(res, {
|
||||
data: {
|
||||
|
||||
@@ -168,13 +168,13 @@ export async function buildClientConfigurationForNewtClient(
|
||||
)
|
||||
);
|
||||
|
||||
const resourceTarget = generateSubnetProxyTargetV2(
|
||||
const resourceTargets = generateSubnetProxyTargetV2(
|
||||
resource,
|
||||
resourceClients
|
||||
);
|
||||
|
||||
if (resourceTarget) {
|
||||
targetsToSend.push(resourceTarget);
|
||||
if (resourceTargets) {
|
||||
targetsToSend.push(...resourceTargets);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ import semver from "semver";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
// Stale-while-revalidate: keeps the last successfully fetched version so that
|
||||
// a transient network failure / timeout does not flip every site back to
|
||||
// newtUpdateAvailable: false.
|
||||
let staleNewtVersion: string | null = null;
|
||||
|
||||
async function getLatestNewtVersion(): Promise<string | null> {
|
||||
try {
|
||||
const cachedVersion = await cache.get<string>("latestNewtVersion");
|
||||
@@ -29,7 +34,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500); // Reduced timeout to 1.5 seconds
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/fosrl/newt/tags",
|
||||
@@ -44,18 +49,46 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
||||
logger.warn(
|
||||
`Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}`
|
||||
);
|
||||
return null;
|
||||
return staleNewtVersion;
|
||||
}
|
||||
|
||||
let tags = await response.json();
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
logger.warn("No tags found for Newt repository");
|
||||
return null;
|
||||
return staleNewtVersion;
|
||||
}
|
||||
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||
|
||||
// Remove release-candidates, then sort descending by semver so that
|
||||
// duplicate tags (e.g. "1.10.3" and "v1.10.3") and any ordering quirks
|
||||
// from the GitHub API do not cause an older tag to be selected.
|
||||
tags = tags.filter((tag: any) => !tag.name.includes("rc"));
|
||||
tags.sort((a: any, b: any) => {
|
||||
const va = semver.coerce(a.name);
|
||||
const vb = semver.coerce(b.name);
|
||||
if (!va && !vb) return 0;
|
||||
if (!va) return 1;
|
||||
if (!vb) return -1;
|
||||
return semver.rcompare(va, vb);
|
||||
});
|
||||
|
||||
// Deduplicate: keep only the first (highest) entry per normalised version
|
||||
const seen = new Set<string>();
|
||||
tags = tags.filter((tag: any) => {
|
||||
const normalised = semver.coerce(tag.name)?.version;
|
||||
if (!normalised || seen.has(normalised)) return false;
|
||||
seen.add(normalised);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (tags.length === 0) {
|
||||
logger.warn("No valid semver tags found for Newt repository");
|
||||
return staleNewtVersion;
|
||||
}
|
||||
|
||||
const latestVersion = tags[0].name;
|
||||
|
||||
await cache.set("latestNewtVersion", latestVersion, 3600);
|
||||
staleNewtVersion = latestVersion;
|
||||
await cache.set("cache:latestNewtVersion", latestVersion, 3600);
|
||||
|
||||
return latestVersion;
|
||||
} catch (error: any) {
|
||||
@@ -73,7 +106,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
||||
error.message || error
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return staleNewtVersion;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -618,11 +618,11 @@ export async function handleMessagingForUpdatedSiteResource(
|
||||
|
||||
// Only update targets on newt if destination changed
|
||||
if (destinationChanged || portRangesChanged) {
|
||||
const oldTarget = generateSubnetProxyTargetV2(
|
||||
const oldTargets = generateSubnetProxyTargetV2(
|
||||
existingSiteResource,
|
||||
mergedAllClients
|
||||
);
|
||||
const newTarget = generateSubnetProxyTargetV2(
|
||||
const newTargets = generateSubnetProxyTargetV2(
|
||||
updatedSiteResource,
|
||||
mergedAllClients
|
||||
);
|
||||
@@ -630,8 +630,8 @@ export async function handleMessagingForUpdatedSiteResource(
|
||||
await updateTargets(
|
||||
newt.newtId,
|
||||
{
|
||||
oldTargets: oldTarget ? [oldTarget] : [],
|
||||
newTargets: newTarget ? [newTarget] : []
|
||||
oldTargets: oldTargets ? oldTargets : [],
|
||||
newTargets: newTargets ? newTargets : []
|
||||
},
|
||||
newt.version
|
||||
);
|
||||
|
||||
@@ -21,7 +21,8 @@ async function queryUser(userId: string) {
|
||||
serverAdmin: users.serverAdmin,
|
||||
idpName: idp.name,
|
||||
idpId: users.idpId,
|
||||
locale: users.locale
|
||||
locale: users.locale,
|
||||
dateCreated: users.dateCreated
|
||||
})
|
||||
.from(users)
|
||||
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||
|
||||
@@ -64,7 +64,8 @@ export async function myDevice(
|
||||
serverAdmin: users.serverAdmin,
|
||||
idpName: idp.name,
|
||||
idpId: users.idpId,
|
||||
locale: users.locale
|
||||
locale: users.locale,
|
||||
dateCreated: users.dateCreated
|
||||
})
|
||||
.from(users)
|
||||
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||
|
||||
@@ -491,6 +491,10 @@ export default function BillingPage() {
|
||||
|
||||
const currentPlanId = getCurrentPlanId();
|
||||
|
||||
const visiblePlanOptions = planOptions.filter(
|
||||
(plan) => plan.id !== "home" || currentPlanId === "home"
|
||||
);
|
||||
|
||||
// Check if subscription is in a problematic state that requires attention
|
||||
const hasProblematicSubscription = (): boolean => {
|
||||
if (!tierSubscription?.subscription) return false;
|
||||
@@ -803,8 +807,8 @@ export default function BillingPage() {
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
{/* Plan Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
{planOptions.filter((plan) => plan.id !== "home" || currentPlanId === "home").map((plan) => {
|
||||
<div className={cn("grid grid-cols-1 gap-4", visiblePlanOptions.length === 5 ? "md:grid-cols-5" : "md:grid-cols-4")}>
|
||||
{visiblePlanOptions.map((plan) => {
|
||||
const isCurrentPlan = plan.id === currentPlanId;
|
||||
const planAction = getPlanAction(plan);
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import { usePaidStatus } from "@/hooks/usePaidStatus";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { toUnicode } from "punycode";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
|
||||
type AvailableOption = {
|
||||
domainNamespaceId: string;
|
||||
@@ -97,10 +98,16 @@ export default function DomainPicker({
|
||||
warnOnProvidedDomain = false
|
||||
}: DomainPickerProps) {
|
||||
const { env } = useEnvContext();
|
||||
const { user } = useUserContext();
|
||||
const api = createApiClient({ env });
|
||||
const t = useTranslations();
|
||||
const { hasSaasSubscription } = usePaidStatus();
|
||||
|
||||
const requiresPaywall =
|
||||
build === "saas" &&
|
||||
!hasSaasSubscription(tierMatrix[TierFeature.DomainNamespaces]) &&
|
||||
new Date(user.dateCreated) > new Date("2026-04-13");
|
||||
|
||||
const { data = [], isLoading: loadingDomains } = useQuery(
|
||||
orgQueries.domains({ orgId })
|
||||
);
|
||||
@@ -656,6 +663,7 @@ export default function DomainPicker({
|
||||
})
|
||||
}
|
||||
className="mx-2 rounded-md"
|
||||
disabled={requiresPaywall}
|
||||
>
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3">
|
||||
<Zap className="h-4 w-4 text-primary" />
|
||||
@@ -696,11 +704,7 @@ export default function DomainPicker({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{build === "saas" &&
|
||||
!hasSaasSubscription(
|
||||
tierMatrix[TierFeature.DomainNamespaces]
|
||||
) &&
|
||||
!hideFreeDomain && (
|
||||
{requiresPaywall && !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">
|
||||
|
||||
Reference in New Issue
Block a user