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;
|
pubKey: string | null;
|
||||||
subnet: string | null;
|
subnet: string | null;
|
||||||
}[]
|
}[]
|
||||||
): SubnetProxyTargetV2 | undefined {
|
): SubnetProxyTargetV2[] | undefined {
|
||||||
if (clients.length === 0) {
|
if (clients.length === 0) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
|
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
|
||||||
@@ -599,7 +599,7 @@ export function generateSubnetProxyTargetV2(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let target: SubnetProxyTargetV2 | null = null;
|
let targets: SubnetProxyTargetV2[] = [];
|
||||||
|
|
||||||
const portRange = [
|
const portRange = [
|
||||||
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
|
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
|
||||||
@@ -614,52 +614,54 @@ export function generateSubnetProxyTargetV2(
|
|||||||
if (ipSchema.safeParse(destination).success) {
|
if (ipSchema.safeParse(destination).success) {
|
||||||
destination = `${destination}/32`;
|
destination = `${destination}/32`;
|
||||||
|
|
||||||
target = {
|
targets.push({
|
||||||
sourcePrefixes: [],
|
sourcePrefixes: [],
|
||||||
destPrefix: destination,
|
destPrefix: destination,
|
||||||
portRange,
|
portRange,
|
||||||
disableIcmp,
|
disableIcmp,
|
||||||
resourceId: siteResource.siteResourceId,
|
resourceId: siteResource.siteResourceId
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (siteResource.alias && siteResource.aliasAddress) {
|
if (siteResource.alias && siteResource.aliasAddress) {
|
||||||
// also push a match for the alias address
|
// also push a match for the alias address
|
||||||
target = {
|
targets.push({
|
||||||
sourcePrefixes: [],
|
sourcePrefixes: [],
|
||||||
destPrefix: `${siteResource.aliasAddress}/32`,
|
destPrefix: `${siteResource.aliasAddress}/32`,
|
||||||
rewriteTo: destination,
|
rewriteTo: destination,
|
||||||
portRange,
|
portRange,
|
||||||
disableIcmp,
|
disableIcmp,
|
||||||
resourceId: siteResource.siteResourceId,
|
resourceId: siteResource.siteResourceId
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
} else if (siteResource.mode == "cidr") {
|
} else if (siteResource.mode == "cidr") {
|
||||||
target = {
|
targets.push({
|
||||||
sourcePrefixes: [],
|
sourcePrefixes: [],
|
||||||
destPrefix: siteResource.destination,
|
destPrefix: siteResource.destination,
|
||||||
portRange,
|
portRange,
|
||||||
disableIcmp,
|
disableIcmp,
|
||||||
resourceId: siteResource.siteResourceId,
|
resourceId: siteResource.siteResourceId
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target) {
|
if (targets.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const clientSite of clients) {
|
for (const target of targets) {
|
||||||
if (!clientSite.subnet) {
|
for (const clientSite of clients) {
|
||||||
logger.debug(
|
if (!clientSite.subnet) {
|
||||||
`Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.`
|
logger.debug(
|
||||||
);
|
`Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.`
|
||||||
continue;
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
|
||||||
|
|
||||||
|
// add client prefix to source prefixes
|
||||||
|
target.sourcePrefixes.push(clientPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
|
|
||||||
|
|
||||||
// add client prefix to source prefixes
|
|
||||||
target.sourcePrefixes.push(clientPrefix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// print a nice representation of the targets
|
// print a nice representation of the targets
|
||||||
@@ -667,36 +669,34 @@ export function generateSubnetProxyTargetV2(
|
|||||||
// `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}`
|
// `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}`
|
||||||
// );
|
// );
|
||||||
|
|
||||||
return target;
|
return targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1)
|
* Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1)
|
||||||
* by expanding each source prefix into its own target entry.
|
* by expanding each source prefix into its own target entry.
|
||||||
* @param targetV2 - The v2 target to convert
|
* @param targetV2 - The v2 target to convert
|
||||||
* @returns Array of v1 SubnetProxyTarget objects
|
* @returns Array of v1 SubnetProxyTarget objects
|
||||||
*/
|
*/
|
||||||
export function convertSubnetProxyTargetsV2ToV1(
|
export function convertSubnetProxyTargetsV2ToV1(
|
||||||
targetsV2: SubnetProxyTargetV2[]
|
targetsV2: SubnetProxyTargetV2[]
|
||||||
): SubnetProxyTarget[] {
|
): SubnetProxyTarget[] {
|
||||||
return targetsV2.flatMap((targetV2) =>
|
return targetsV2.flatMap((targetV2) =>
|
||||||
targetV2.sourcePrefixes.map((sourcePrefix) => ({
|
targetV2.sourcePrefixes.map((sourcePrefix) => ({
|
||||||
sourcePrefix,
|
sourcePrefix,
|
||||||
destPrefix: targetV2.destPrefix,
|
destPrefix: targetV2.destPrefix,
|
||||||
...(targetV2.disableIcmp !== undefined && {
|
...(targetV2.disableIcmp !== undefined && {
|
||||||
disableIcmp: targetV2.disableIcmp
|
disableIcmp: targetV2.disableIcmp
|
||||||
}),
|
}),
|
||||||
...(targetV2.rewriteTo !== undefined && {
|
...(targetV2.rewriteTo !== undefined && {
|
||||||
rewriteTo: targetV2.rewriteTo
|
rewriteTo: targetV2.rewriteTo
|
||||||
}),
|
}),
|
||||||
...(targetV2.portRange !== undefined && {
|
...(targetV2.portRange !== undefined && {
|
||||||
portRange: targetV2.portRange
|
portRange: targetV2.portRange
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Custom schema for validating port range strings
|
// Custom schema for validating port range strings
|
||||||
// Format: "80,443,8000-9000" or "*" for all ports, or empty string
|
// Format: "80,443,8000-9000" or "*" for all ports, or empty string
|
||||||
|
|||||||
@@ -661,16 +661,16 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (addedClients.length > 0) {
|
if (addedClients.length > 0) {
|
||||||
const targetToAdd = generateSubnetProxyTargetV2(
|
const targetsToAdd = generateSubnetProxyTargetV2(
|
||||||
siteResource,
|
siteResource,
|
||||||
addedClients
|
addedClients
|
||||||
);
|
);
|
||||||
|
|
||||||
if (targetToAdd) {
|
if (targetsToAdd) {
|
||||||
proxyJobs.push(
|
proxyJobs.push(
|
||||||
addSubnetProxyTargets(
|
addSubnetProxyTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
[targetToAdd],
|
targetsToAdd,
|
||||||
newt.version
|
newt.version
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -698,16 +698,16 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (removedClients.length > 0) {
|
if (removedClients.length > 0) {
|
||||||
const targetToRemove = generateSubnetProxyTargetV2(
|
const targetsToRemove = generateSubnetProxyTargetV2(
|
||||||
siteResource,
|
siteResource,
|
||||||
removedClients
|
removedClients
|
||||||
);
|
);
|
||||||
|
|
||||||
if (targetToRemove) {
|
if (targetsToRemove) {
|
||||||
proxyJobs.push(
|
proxyJobs.push(
|
||||||
removeSubnetProxyTargets(
|
removeSubnetProxyTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
[targetToRemove],
|
targetsToRemove,
|
||||||
newt.version
|
newt.version
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -1164,7 +1164,7 @@ async function handleMessagesForClientResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
const target = generateSubnetProxyTargetV2(resource, [
|
const targets = generateSubnetProxyTargetV2(resource, [
|
||||||
{
|
{
|
||||||
clientId: client.clientId,
|
clientId: client.clientId,
|
||||||
pubKey: client.pubKey,
|
pubKey: client.pubKey,
|
||||||
@@ -1172,11 +1172,11 @@ async function handleMessagesForClientResources(
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (target) {
|
if (targets) {
|
||||||
proxyJobs.push(
|
proxyJobs.push(
|
||||||
addSubnetProxyTargets(
|
addSubnetProxyTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
[target],
|
targets,
|
||||||
newt.version
|
newt.version
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -1241,7 +1241,7 @@ async function handleMessagesForClientResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
const target = generateSubnetProxyTargetV2(resource, [
|
const targets = generateSubnetProxyTargetV2(resource, [
|
||||||
{
|
{
|
||||||
clientId: client.clientId,
|
clientId: client.clientId,
|
||||||
pubKey: client.pubKey,
|
pubKey: client.pubKey,
|
||||||
@@ -1249,11 +1249,11 @@ async function handleMessagesForClientResources(
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (target) {
|
if (targets) {
|
||||||
proxyJobs.push(
|
proxyJobs.push(
|
||||||
removeSubnetProxyTargets(
|
removeSubnetProxyTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
[target],
|
targets,
|
||||||
newt.version
|
newt.version
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,65 +29,9 @@ import {
|
|||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import NodeCache from "node-cache";
|
|
||||||
import semver from "semver";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
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({
|
const listClientsParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
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(
|
// const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map(
|
||||||
(client) => {
|
// (client) => {
|
||||||
const OlmWithUpdate: OlmWithUpdateAvailable = { ...client };
|
// const OlmWithUpdate: OlmWithUpdateAvailable = { ...client };
|
||||||
// Initially set to false, will be updated if version check succeeds
|
// // Initially set to false, will be updated if version check succeeds
|
||||||
OlmWithUpdate.olmUpdateAvailable = false;
|
// OlmWithUpdate.olmUpdateAvailable = false;
|
||||||
return OlmWithUpdate;
|
// return OlmWithUpdate;
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
// Try to get the latest version, but don't block if it fails
|
// Try to get the latest version, but don't block if it fails
|
||||||
try {
|
// try {
|
||||||
const latestOlVersion = await latestOlVersionPromise;
|
// const latestOlmVersion = await latestOlVersionPromise;
|
||||||
|
|
||||||
if (latestOlVersion) {
|
// if (latestOlVersion) {
|
||||||
olmsWithUpdates.forEach((client) => {
|
// olmsWithUpdates.forEach((client) => {
|
||||||
try {
|
// try {
|
||||||
client.olmUpdateAvailable = semver.lt(
|
// client.olmUpdateAvailable = semver.lt(
|
||||||
client.olmVersion ? client.olmVersion : "",
|
// client.olmVersion ? client.olmVersion : "",
|
||||||
latestOlVersion
|
// latestOlVersion
|
||||||
);
|
// );
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
client.olmUpdateAvailable = false;
|
// client.olmUpdateAvailable = false;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
// Log the error but don't let it block the response
|
// // Log the error but don't let it block the response
|
||||||
logger.warn(
|
// logger.warn(
|
||||||
"Failed to check for OLM updates, continuing without update info:",
|
// "Failed to check for OLM updates, continuing without update info:",
|
||||||
error
|
// error
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
return response<ListClientsResponse>(res, {
|
return response<ListClientsResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
clients: olmsWithUpdates,
|
clients: clientsWithSites,
|
||||||
pagination: {
|
pagination: {
|
||||||
total: totalCount,
|
total: totalCount,
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -30,65 +30,10 @@ import {
|
|||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import NodeCache from "node-cache";
|
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
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({
|
const listUserDevicesParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
});
|
});
|
||||||
@@ -453,29 +398,30 @@ export async function listUserDevices(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Try to get the latest version, but don't block if it fails
|
// REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW
|
||||||
try {
|
// // Try to get the latest version, but don't block if it fails
|
||||||
const latestOlmVersion = await getLatestOlmVersion();
|
// try {
|
||||||
|
// const latestOlmVersion = await getLatestOlmVersion();
|
||||||
|
|
||||||
if (latestOlmVersion) {
|
// if (latestOlmVersion) {
|
||||||
olmsWithUpdates.forEach((client) => {
|
// olmsWithUpdates.forEach((client) => {
|
||||||
try {
|
// try {
|
||||||
client.olmUpdateAvailable = semver.lt(
|
// client.olmUpdateAvailable = semver.lt(
|
||||||
client.olmVersion ? client.olmVersion : "",
|
// client.olmVersion ? client.olmVersion : "",
|
||||||
latestOlmVersion
|
// latestOlmVersion
|
||||||
);
|
// );
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
client.olmUpdateAvailable = false;
|
// client.olmUpdateAvailable = false;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
// Log the error but don't let it block the response
|
// // Log the error but don't let it block the response
|
||||||
logger.warn(
|
// logger.warn(
|
||||||
"Failed to check for OLM updates, continuing without update info:",
|
// "Failed to check for OLM updates, continuing without update info:",
|
||||||
error
|
// error
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
return response<ListUserDevicesResponse>(res, {
|
return response<ListUserDevicesResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -168,13 +168,13 @@ export async function buildClientConfigurationForNewtClient(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const resourceTarget = generateSubnetProxyTargetV2(
|
const resourceTargets = generateSubnetProxyTargetV2(
|
||||||
resource,
|
resource,
|
||||||
resourceClients
|
resourceClients
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resourceTarget) {
|
if (resourceTargets) {
|
||||||
targetsToSend.push(resourceTarget);
|
targetsToSend.push(...resourceTargets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ import semver from "semver";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
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> {
|
async function getLatestNewtVersion(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const cachedVersion = await cache.get<string>("latestNewtVersion");
|
const cachedVersion = await cache.get<string>("latestNewtVersion");
|
||||||
@@ -29,7 +34,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
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(
|
const response = await fetch(
|
||||||
"https://api.github.com/repos/fosrl/newt/tags",
|
"https://api.github.com/repos/fosrl/newt/tags",
|
||||||
@@ -44,18 +49,46 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}`
|
`Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}`
|
||||||
);
|
);
|
||||||
return null;
|
return staleNewtVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tags = await response.json();
|
let tags = await response.json();
|
||||||
if (!Array.isArray(tags) || tags.length === 0) {
|
if (!Array.isArray(tags) || tags.length === 0) {
|
||||||
logger.warn("No tags found for Newt repository");
|
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;
|
const latestVersion = tags[0].name;
|
||||||
|
|
||||||
await cache.set("latestNewtVersion", latestVersion, 3600);
|
staleNewtVersion = latestVersion;
|
||||||
|
await cache.set("cache:latestNewtVersion", latestVersion, 3600);
|
||||||
|
|
||||||
return latestVersion;
|
return latestVersion;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -73,7 +106,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
|||||||
error.message || error
|
error.message || error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return staleNewtVersion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -618,11 +618,11 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
|
|
||||||
// Only update targets on newt if destination changed
|
// Only update targets on newt if destination changed
|
||||||
if (destinationChanged || portRangesChanged) {
|
if (destinationChanged || portRangesChanged) {
|
||||||
const oldTarget = generateSubnetProxyTargetV2(
|
const oldTargets = generateSubnetProxyTargetV2(
|
||||||
existingSiteResource,
|
existingSiteResource,
|
||||||
mergedAllClients
|
mergedAllClients
|
||||||
);
|
);
|
||||||
const newTarget = generateSubnetProxyTargetV2(
|
const newTargets = generateSubnetProxyTargetV2(
|
||||||
updatedSiteResource,
|
updatedSiteResource,
|
||||||
mergedAllClients
|
mergedAllClients
|
||||||
);
|
);
|
||||||
@@ -630,8 +630,8 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
await updateTargets(
|
await updateTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
{
|
{
|
||||||
oldTargets: oldTarget ? [oldTarget] : [],
|
oldTargets: oldTargets ? oldTargets : [],
|
||||||
newTargets: newTarget ? [newTarget] : []
|
newTargets: newTargets ? newTargets : []
|
||||||
},
|
},
|
||||||
newt.version
|
newt.version
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ async function queryUser(userId: string) {
|
|||||||
serverAdmin: users.serverAdmin,
|
serverAdmin: users.serverAdmin,
|
||||||
idpName: idp.name,
|
idpName: idp.name,
|
||||||
idpId: users.idpId,
|
idpId: users.idpId,
|
||||||
locale: users.locale
|
locale: users.locale,
|
||||||
|
dateCreated: users.dateCreated
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ export async function myDevice(
|
|||||||
serverAdmin: users.serverAdmin,
|
serverAdmin: users.serverAdmin,
|
||||||
idpName: idp.name,
|
idpName: idp.name,
|
||||||
idpId: users.idpId,
|
idpId: users.idpId,
|
||||||
locale: users.locale
|
locale: users.locale,
|
||||||
|
dateCreated: users.dateCreated
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||||
|
|||||||
@@ -491,6 +491,10 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
const currentPlanId = getCurrentPlanId();
|
const currentPlanId = getCurrentPlanId();
|
||||||
|
|
||||||
|
const visiblePlanOptions = planOptions.filter(
|
||||||
|
(plan) => plan.id !== "home" || currentPlanId === "home"
|
||||||
|
);
|
||||||
|
|
||||||
// Check if subscription is in a problematic state that requires attention
|
// Check if subscription is in a problematic state that requires attention
|
||||||
const hasProblematicSubscription = (): boolean => {
|
const hasProblematicSubscription = (): boolean => {
|
||||||
if (!tierSubscription?.subscription) return false;
|
if (!tierSubscription?.subscription) return false;
|
||||||
@@ -803,8 +807,8 @@ export default function BillingPage() {
|
|||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
{/* Plan Cards Grid */}
|
{/* Plan Cards Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
<div className={cn("grid grid-cols-1 gap-4", visiblePlanOptions.length === 5 ? "md:grid-cols-5" : "md:grid-cols-4")}>
|
||||||
{planOptions.filter((plan) => plan.id !== "home" || currentPlanId === "home").map((plan) => {
|
{visiblePlanOptions.map((plan) => {
|
||||||
const isCurrentPlan = plan.id === currentPlanId;
|
const isCurrentPlan = plan.id === currentPlanId;
|
||||||
const planAction = getPlanAction(plan);
|
const planAction = getPlanAction(plan);
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { usePaidStatus } from "@/hooks/usePaidStatus";
|
|||||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
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";
|
||||||
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
|
|
||||||
type AvailableOption = {
|
type AvailableOption = {
|
||||||
domainNamespaceId: string;
|
domainNamespaceId: string;
|
||||||
@@ -97,10 +98,16 @@ export default function DomainPicker({
|
|||||||
warnOnProvidedDomain = false
|
warnOnProvidedDomain = false
|
||||||
}: DomainPickerProps) {
|
}: DomainPickerProps) {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
|
const { user } = useUserContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { hasSaasSubscription } = usePaidStatus();
|
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(
|
const { data = [], isLoading: loadingDomains } = useQuery(
|
||||||
orgQueries.domains({ orgId })
|
orgQueries.domains({ orgId })
|
||||||
);
|
);
|
||||||
@@ -656,6 +663,7 @@ export default function DomainPicker({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="mx-2 rounded-md"
|
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">
|
<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" />
|
<Zap className="h-4 w-4 text-primary" />
|
||||||
@@ -696,11 +704,7 @@ export default function DomainPicker({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{build === "saas" &&
|
{requiresPaywall && !hideFreeDomain && (
|
||||||
!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">
|
<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">
|
<CardContent className="py-3 px-4">
|
||||||
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
||||||
|
|||||||
Reference in New Issue
Block a user