Compare commits

...

9 Commits

Author SHA1 Message Date
Owen 1b89c328de Merge branch 'main' into dev 2026-07-29 17:54:59 -04:00
miloschwartz e7f147d4ca fix site status stuck loading sometimes 2026-07-29 17:52:55 -04:00
miloschwartz 41dbc7d5ed remove time logs 2026-07-29 17:45:19 -04:00
miloschwartz 9f297317f2 bump version 2026-07-29 17:33:49 -04:00
miloschwartz 864a6a39cd Merge branch 'main' into dev 2026-07-29 17:27:22 -04:00
miloschwartz ef3f71e9b5 Merge branch 'main' into dev 2026-07-29 17:09:54 -04:00
miloschwartz f47c94d05b check idp org ownership on save policy closes #3290 2026-07-29 09:38:15 -04:00
Owen 9a9ae649ef Add default to path
Fix #3484
2026-07-27 10:02:32 -04:00
Owen aa6dc67015 Pull the version on the info page and use api 2026-07-27 10:01:12 -04:00
10 changed files with 80 additions and 95 deletions
+7 -14
View File
@@ -1,7 +1,5 @@
import {
db,
idp,
idpOrg,
resourcePolicies,
resourcePolicyHeaderAuth,
resourcePolicyPassword,
@@ -20,6 +18,7 @@ import { Config, ResourcePolicyData } from "./types";
import logger from "@server/logger";
import { getUniqueResourcePolicyName } from "@server/db/names";
import { hashPassword } from "@server/auth/password";
import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
@@ -71,19 +70,13 @@ export async function updateResourcePolicies(
// Validate auto-login-idp if provided
if (policyData["auto-login-idp"]) {
const [provider] = await trx
.select()
.from(idp)
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
.where(
and(
eq(idp.idpId, policyData["auto-login-idp"]),
eq(idpOrg.orgId, orgId)
)
)
.limit(1);
const providerExists = await idpExistsForOrg(
policyData["auto-login-idp"],
orgId,
trx
);
if (!provider) {
if (!providerExists) {
throw new Error(
`Identity provider not found for policy '${policyNiceId}' in this organization`
);
+1 -1
View File
@@ -28,7 +28,7 @@ export const TargetHealthCheckSchema = z.object({
hostname: z.string(),
port: z.int().min(1).max(65535),
enabled: z.boolean().optional().default(true),
path: z.string().optional(),
path: z.string().optional().default("/"),
scheme: z.string().optional(),
mode: z.string().default("http"),
interval: z.int().default(30),
+1 -1
View File
@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process
export const APP_VERSION = "1.21.0";
export const APP_VERSION = "1.21.1";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);
+36
View File
@@ -0,0 +1,36 @@
import { db, idp, idpOrg, Transaction } from "@server/db";
import { and, eq } from "drizzle-orm";
export function isOrgIdentityProviderMode(): boolean {
return process.env.IDENTITY_PROVIDER_MODE === "org";
}
/**
* Checks whether an identity provider can be used for the given org.
* In org IdP mode, the provider must be linked via idpOrg.
* In global IdP mode, the provider only needs to exist.
*/
export async function idpExistsForOrg(
idpId: number,
orgId: string,
dbOrTrx: typeof db | Transaction = db
): Promise<boolean> {
if (isOrgIdentityProviderMode()) {
const [provider] = await dbOrTrx
.select({ idpId: idp.idpId })
.from(idp)
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
.where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId)))
.limit(1);
return !!provider;
}
const [provider] = await dbOrTrx
.select({ idpId: idp.idpId })
.from(idp)
.where(eq(idp.idpId, idpId))
.limit(1);
return !!provider;
}
-14
View File
@@ -41,7 +41,6 @@ export async function getCachedStatusHistory(
return cached;
}
console.time(`[getCachedStatusHistory/${entityType}=${entityId}]`);
// Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query
// window aligns with stable calendar days for the requesting client
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
@@ -77,14 +76,12 @@ export async function getCachedStatusHistory(
const priorStatus = lastKnownEvent?.status ?? null;
console.time(`[computeBuckets/${entityType}=${entityId}]`);
const { buckets, totalDowntime } = computeBuckets(
events,
days,
priorStatus,
tzOffsetMinutes
);
console.timeEnd(`[computeBuckets/${entityType}=${entityId}]`);
const totalWindow = days * 86400;
const overallUptime =
totalWindow > 0
@@ -99,7 +96,6 @@ export async function getCachedStatusHistory(
totalDowntimeSeconds: totalDowntime
};
console.timeEnd(`[getCachedStatusHistory/${entityType}=${entityId}]`);
await cache.set(cacheKey, result, STATUS_HISTORY_CACHE_TTL);
return result;
}
@@ -317,10 +313,6 @@ export async function getBatchedStatusHistory(
days: number,
tzOffsetMinutes: number = 0
): Promise<BatchedStatusHistoryResponse> {
console.time(
`[getBatchedStatusHistory/${entityType}=(${entityIds.join(" ,")})]`
);
// Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query
// window aligns with stable calendar days for the requesting client
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
@@ -398,7 +390,6 @@ export async function getBatchedStatusHistory(
const result: BatchedStatusHistoryResponse = {};
console.time(`[computeBuckets/${entityType}=(${entityIds.join(" ,")})]`);
for (const entityId in eventStatusMap) {
const event = eventStatusMap[Number(entityId)];
const priorStatus = event.lastKnownEvent?.status ?? null;
@@ -426,10 +417,5 @@ export async function getBatchedStatusHistory(
totalDowntimeSeconds: totalDowntime
};
}
console.timeEnd(`[computeBuckets/${entityType}=(${entityIds.join(" ,")})]`);
console.timeEnd(
`[getBatchedStatusHistory/${entityType}=(${entityIds.join(" ,")})]`
);
return result;
}
@@ -14,8 +14,6 @@
import { hashPassword } from "@server/auth/password";
import {
db,
idp,
idpOrg,
orgs,
resourcePolicies,
resourcePolicyHeaderAuth,
@@ -31,6 +29,7 @@ import {
type ResourcePolicy
} from "@server/db";
import { getUniqueResourcePolicyName } from "@server/db/names";
import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg";
import response from "@server/lib/response";
import {
getResourceRuleValueValidationError,
@@ -204,14 +203,9 @@ export async function createResourcePolicy(
// Check if Identity provider in `skipToIdpId` exists
if (skipToIdpId) {
const [provider] = await db
.select()
.from(idp)
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
.where(and(eq(idp.idpId, skipToIdpId), eq(idpOrg.orgId, orgId)))
.limit(1);
const providerExists = await idpExistsForOrg(skipToIdpId, orgId);
if (!provider) {
if (!providerExists) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
@@ -2,8 +2,6 @@ import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
db,
idp,
idpOrg,
resourcePolicies,
rolePolicies,
roles,
@@ -18,6 +16,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { and, eq, inArray, ne } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg";
const setResourcePolicyAcccessControlBodySchema = z.strictObject({
sso: z.boolean(),
@@ -27,7 +26,7 @@ const setResourcePolicyAcccessControlBodySchema = z.strictObject({
}),
skipToIdpId: z.int().positive().optional().nullable().openapi({
type: "integer",
description: "Page number to retrieve"
description: "Default identity provider ID to skip to on login"
})
});
@@ -36,8 +35,8 @@ const setResourcePolicyAccessControlParamsSchema = z.strictObject({
});
registry.registerPath({
method: "post",
path: "/resource-policy/{resourceId}/access-control",
method: "put",
path: "/resource-policy/{resourcePolicyId}/access-control",
description:
"Set access control users for a resource policy, including SSO, users, roles, Identity provider.",
tags: [OpenAPITags.PublicResourcePolicyLegacy],
@@ -163,16 +162,9 @@ export async function setResourcePolicyAccessControl(
// Check if Identity provider in `skipToIdpId` exists
if (idpId) {
const [provider] = await db
.select()
.from(idp)
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
.where(
and(eq(idp.idpId, idpId), eq(idpOrg.orgId, policy.orgId))
)
.limit(1);
const providerExists = await idpExistsForOrg(idpId, policy.orgId);
if (!provider) {
if (!providerExists) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
@@ -40,6 +40,8 @@ import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import type { AxiosResponse } from "axios";
import { useQuery } from "@tanstack/react-query";
import { productUpdatesQueries } from "@app/lib/queries";
export default function CredentialsPage() {
const { env } = useEnvContext();
@@ -67,6 +69,12 @@ export default function CredentialsPage() {
const { isPaidUser } = usePaidStatus();
const { data: latestVersions } = useQuery(
productUpdatesQueries.latestVersion(true)
);
const newtVersion =
latestVersions?.data?.newt?.latestVersion ?? "latest";
// Fetch site defaults for wireguard sites to show in obfuscated config
useEffect(() => {
const fetchSiteDefaults = async () => {
@@ -302,6 +310,7 @@ export default function CredentialsPage() {
id={displayNewtId ?? "**********"}
secret={displaySecret ?? "**************"}
endpoint={env.app.dashboardUrl}
version={newtVersion}
/>
</>
)}
+8 -40
View File
@@ -56,6 +56,8 @@ import { QRCodeCanvas } from "qrcode.react";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
import { useQuery } from "@tanstack/react-query";
import { productUpdatesQueries } from "@app/lib/queries";
type SiteType = "newt" | "wireguard" | "local";
@@ -189,9 +191,14 @@ export default function Page() {
const [wgConfig, setWgConfig] = useState("");
const [createLoading, setCreateLoading] = useState(false);
const [newtVersion, setNewtVersion] = useState("latest");
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const { data: latestVersions } = useQuery(
productUpdatesQueries.latestVersion(true)
);
const newtVersion =
latestVersions?.data?.newt?.latestVersion ?? "latest";
const [siteDefaults, setSiteDefaults] =
useState<PickSiteDefaultsResponse | null>(null);
@@ -302,45 +309,6 @@ export default function Page() {
const load = async () => {
setLoadingPage(true);
let currentNewtVersion = "latest";
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const response = await fetch(
`https://api.github.com/repos/fosrl/newt/releases/latest`,
{ signal: controller.signal }
);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(
t("newtErrorFetchReleases", {
err: response.statusText
})
);
}
const data = await response.json();
const latestVersion = data.tag_name;
currentNewtVersion = latestVersion;
setNewtVersion(latestVersion);
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
console.error(t("newtErrorFetchTimeout"));
} else {
console.error(
t("newtErrorFetchLatest", {
err:
error instanceof Error
? error.message
: String(error)
})
);
}
}
const generatedKeypair = generateKeypair();
const privateKey = generatedKeypair.privateKey;
+9 -2
View File
@@ -114,7 +114,7 @@ export default function SitesTable({
const [isRefreshing, startTransition] = useTransition();
const [isNavigatingToAddPage, startNavigation] = useTransition();
const siteIds = sites.map((s) => s.id);
const siteIds = useMemo(() => sites.map((s) => s.id), [sites]);
const statusHistoryQuery = useQuery({
...orgQueries.batchedSiteStatusHistory({
@@ -638,7 +638,14 @@ export default function SitesTable({
];
return cols;
}, [orgId, t, searchParams, latestNewtVersion]);
}, [
orgId,
t,
searchParams,
latestNewtVersion,
statusHistoryQuery.data,
statusHistoryQuery.isLoading
]);
function toggleSort(column: string) {
const newSearch = getNextSortOrder(column, searchParams);