Merge pull request #2151 from fosrl/dev

1.14.0 ready
This commit is contained in:
Milo Schwartz
2025-12-22 13:16:30 -08:00
committed by GitHub
15 changed files with 242 additions and 177 deletions

View File

@@ -2349,6 +2349,7 @@
"enterConfirmation": "Enter confirmation", "enterConfirmation": "Enter confirmation",
"blueprintViewDetails": "Details", "blueprintViewDetails": "Details",
"defaultIdentityProvider": "Default Identity Provider", "defaultIdentityProvider": "Default Identity Provider",
"defaultIdentityProviderDescription": "When a default identity provider is selected, the user will be automatically redirected to the provider for authentication.",
"editInternalResourceDialogNetworkSettings": "Network Settings", "editInternalResourceDialogNetworkSettings": "Network Settings",
"editInternalResourceDialogAccessPolicy": "Access Policy", "editInternalResourceDialogAccessPolicy": "Access Policy",
"editInternalResourceDialogAddRoles": "Add Roles", "editInternalResourceDialogAddRoles": "Add Roles",

View File

@@ -2,7 +2,8 @@ import {
domains, domains,
orgDomains, orgDomains,
Resource, Resource,
resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility, resourceHeaderAuth,
resourceHeaderAuthExtendedCompatibility,
resourcePincode, resourcePincode,
resourceRules, resourceRules,
resourceWhitelist, resourceWhitelist,
@@ -16,8 +17,8 @@ import {
userResources, userResources,
users users
} from "@server/db"; } from "@server/db";
import {resources, targets, sites} from "@server/db"; import { resources, targets, sites } from "@server/db";
import {eq, and, asc, or, ne, count, isNotNull} from "drizzle-orm"; import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
import { import {
Config, Config,
ConfigSchema, ConfigSchema,
@@ -25,12 +26,13 @@ import {
TargetData TargetData
} from "./types"; } from "./types";
import logger from "@server/logger"; import logger from "@server/logger";
import {createCertificate} from "#dynamic/routers/certificates/createCertificate"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import {pickPort} from "@server/routers/target/helpers"; import { pickPort } from "@server/routers/target/helpers";
import {resourcePassword} from "@server/db"; import { resourcePassword } from "@server/db";
import {hashPassword} from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import {isValidCIDR, isValidIP, isValidUrlGlobPattern} from "../validators"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import {get} from "http"; import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
import { build } from "@server/build";
export type ProxyResourcesResults = { export type ProxyResourcesResults = {
proxyResource: Resource; proxyResource: Resource;
@@ -63,7 +65,7 @@ export async function updateProxyResources(
if (targetSiteId) { if (targetSiteId) {
// Look up site by niceId // Look up site by niceId
[site] = await trx [site] = await trx
.select({siteId: sites.siteId}) .select({ siteId: sites.siteId })
.from(sites) .from(sites)
.where( .where(
and( and(
@@ -75,7 +77,7 @@ export async function updateProxyResources(
} else if (siteId) { } else if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org // Use the provided siteId directly, but verify it belongs to the org
[site] = await trx [site] = await trx
.select({siteId: sites.siteId}) .select({ siteId: sites.siteId })
.from(sites) .from(sites)
.where( .where(
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
@@ -93,7 +95,7 @@ export async function updateProxyResources(
let internalPortToCreate; let internalPortToCreate;
if (!targetData["internal-port"]) { if (!targetData["internal-port"]) {
const {internalPort, targetIps} = await pickPort( const { internalPort, targetIps } = await pickPort(
site.siteId!, site.siteId!,
trx trx
); );
@@ -209,6 +211,16 @@ export async function updateProxyResources(
resource = existingResource; resource = existingResource;
} else { } else {
// Update existing resource // Update existing resource
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
resourceData.maintenance = undefined;
}
[resource] = await trx [resource] = await trx
.update(resources) .update(resources)
.set({ .set({
@@ -228,12 +240,19 @@ export async function updateProxyResources(
tlsServerName: resourceData["tls-server-name"] || null, tlsServerName: resourceData["tls-server-name"] || null,
emailWhitelistEnabled: resourceData.auth?.[ emailWhitelistEnabled: resourceData.auth?.[
"whitelist-users" "whitelist-users"
] ]
? resourceData.auth["whitelist-users"].length > 0 ? resourceData.auth["whitelist-users"].length > 0
: false, : false,
headers: headers || null, headers: headers || null,
applyRules: applyRules:
resourceData.rules && resourceData.rules.length > 0 resourceData.rules && resourceData.rules.length > 0,
maintenanceModeEnabled:
resourceData.maintenance?.enabled,
maintenanceModeType: resourceData.maintenance?.type,
maintenanceTitle: resourceData.maintenance?.title,
maintenanceMessage: resourceData.maintenance?.message,
maintenanceEstimatedTime:
resourceData.maintenance?.["estimated-time"]
}) })
.where( .where(
eq(resources.resourceId, existingResource.resourceId) eq(resources.resourceId, existingResource.resourceId)
@@ -303,8 +322,13 @@ export async function updateProxyResources(
const headerAuthPassword = const headerAuthPassword =
resourceData.auth?.["basic-auth"]?.password; resourceData.auth?.["basic-auth"]?.password;
const headerAuthExtendedCompatibility = const headerAuthExtendedCompatibility =
resourceData.auth?.["basic-auth"]?.extendedCompatibility; resourceData.auth?.["basic-auth"]
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { ?.extendedCompatibility;
if (
headerAuthUser &&
headerAuthPassword &&
headerAuthExtendedCompatibility !== null
) {
const headerAuthHash = await hashPassword( const headerAuthHash = await hashPassword(
Buffer.from( Buffer.from(
`${headerAuthUser}:${headerAuthPassword}` `${headerAuthUser}:${headerAuthPassword}`
@@ -315,10 +339,13 @@ export async function updateProxyResources(
resourceId: existingResource.resourceId, resourceId: existingResource.resourceId,
headerAuthHash headerAuthHash
}), }),
trx.insert(resourceHeaderAuthExtendedCompatibility).values({ trx
resourceId: existingResource.resourceId, .insert(resourceHeaderAuthExtendedCompatibility)
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility .values({
}) resourceId: existingResource.resourceId,
extendedCompatibilityIsActivated:
headerAuthExtendedCompatibility
})
]); ]);
} }
} }
@@ -380,7 +407,7 @@ export async function updateProxyResources(
if (targetSiteId) { if (targetSiteId) {
// Look up site by niceId // Look up site by niceId
[site] = await trx [site] = await trx
.select({siteId: sites.siteId}) .select({ siteId: sites.siteId })
.from(sites) .from(sites)
.where( .where(
and( and(
@@ -392,7 +419,7 @@ export async function updateProxyResources(
} else if (siteId) { } else if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org // Use the provided siteId directly, but verify it belongs to the org
[site] = await trx [site] = await trx
.select({siteId: sites.siteId}) .select({ siteId: sites.siteId })
.from(sites) .from(sites)
.where( .where(
and( and(
@@ -437,7 +464,7 @@ export async function updateProxyResources(
if (checkIfTargetChanged(existingTarget, updatedTarget)) { if (checkIfTargetChanged(existingTarget, updatedTarget)) {
let internalPortToUpdate; let internalPortToUpdate;
if (!targetData["internal-port"]) { if (!targetData["internal-port"]) {
const {internalPort, targetIps} = await pickPort( const { internalPort, targetIps } = await pickPort(
site.siteId!, site.siteId!,
trx trx
); );
@@ -622,6 +649,15 @@ export async function updateProxyResources(
); );
} }
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
resourceData.maintenance = undefined;
}
// Create new resource // Create new resource
const [newResource] = await trx const [newResource] = await trx
.insert(resources) .insert(resources)
@@ -643,7 +679,13 @@ export async function updateProxyResources(
ssl: resourceSsl, ssl: resourceSsl,
headers: headers || null, headers: headers || null,
applyRules: applyRules:
resourceData.rules && resourceData.rules.length > 0 resourceData.rules && resourceData.rules.length > 0,
maintenanceModeEnabled: resourceData.maintenance?.enabled,
maintenanceModeType: resourceData.maintenance?.type,
maintenanceTitle: resourceData.maintenance?.title,
maintenanceMessage: resourceData.maintenance?.message,
maintenanceEstimatedTime:
resourceData.maintenance?.["estimated-time"]
}) })
.returning(); .returning();
@@ -674,9 +716,14 @@ export async function updateProxyResources(
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user; const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
const headerAuthPassword = const headerAuthPassword =
resourceData.auth?.["basic-auth"]?.password; resourceData.auth?.["basic-auth"]?.password;
const headerAuthExtendedCompatibility = resourceData.auth?.["basic-auth"]?.extendedCompatibility; const headerAuthExtendedCompatibility =
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { if (
headerAuthUser &&
headerAuthPassword &&
headerAuthExtendedCompatibility !== null
) {
const headerAuthHash = await hashPassword( const headerAuthHash = await hashPassword(
Buffer.from( Buffer.from(
`${headerAuthUser}:${headerAuthPassword}` `${headerAuthUser}:${headerAuthPassword}`
@@ -688,10 +735,13 @@ export async function updateProxyResources(
resourceId: newResource.resourceId, resourceId: newResource.resourceId,
headerAuthHash headerAuthHash
}), }),
trx.insert(resourceHeaderAuthExtendedCompatibility).values({ trx
resourceId: newResource.resourceId, .insert(resourceHeaderAuthExtendedCompatibility)
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility .values({
}), resourceId: newResource.resourceId,
extendedCompatibilityIsActivated:
headerAuthExtendedCompatibility
})
]); ]);
} }
} }
@@ -1043,7 +1093,7 @@ async function getDomain(
trx: Transaction trx: Transaction
) { ) {
const [fullDomainExists] = await trx const [fullDomainExists] = await trx
.select({resourceId: resources.resourceId}) .select({ resourceId: resources.resourceId })
.from(resources) .from(resources)
.where( .where(
and( and(

View File

@@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { portRangeStringSchema } from "@server/lib/ip"; import { portRangeStringSchema } from "@server/lib/ip";
import { MaintenanceSchema } from "#dynamic/lib/blueprints/types";
export const SiteSchema = z.object({ export const SiteSchema = z.object({
name: z.string().min(1).max(100), name: z.string().min(1).max(100),
@@ -156,7 +157,8 @@ export const ResourceSchema = z
"host-header": z.string().optional(), "host-header": z.string().optional(),
"tls-server-name": z.string().optional(), "tls-server-name": z.string().optional(),
headers: z.array(HeaderSchema).optional(), headers: z.array(HeaderSchema).optional(),
rules: z.array(RuleSchema).optional() rules: z.array(RuleSchema).optional(),
maintenance: MaintenanceSchema.optional()
}) })
.refine( .refine(
(resource) => { (resource) => {

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
export const MaintenanceSchema = z.object({
enabled: z.boolean().optional(),
type: z.enum(["forced", "automatic"]).optional(),
title: z.string().max(255).nullable().optional(),
message: z.string().max(2000).nullable().optional(),
"estimated-time": z.string().max(100).nullable().optional()
});

View File

@@ -105,6 +105,7 @@ async function query(query: Q) {
// throw an error // throw an error
throw createHttpError( throw createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
// todo: is this even possible?
`Too many distinct countries. Please narrow your query.` `Too many distinct countries. Please narrow your query.`
); );
} }

View File

@@ -219,15 +219,17 @@ async function queryUniqueFilterAttributes(
.limit(DISTINCT_LIMIT+1) .limit(DISTINCT_LIMIT+1)
]); ]);
if ( // TODO: for stuff like the paths this is too restrictive so lets just show some of the paths and the user needs to
uniqueActors.length > DISTINCT_LIMIT || // refine the time range to see what they need to see
uniqueLocations.length > DISTINCT_LIMIT || // if (
uniqueHosts.length > DISTINCT_LIMIT || // uniqueActors.length > DISTINCT_LIMIT ||
uniquePaths.length > DISTINCT_LIMIT || // uniqueLocations.length > DISTINCT_LIMIT ||
uniqueResources.length > DISTINCT_LIMIT // uniqueHosts.length > DISTINCT_LIMIT ||
) { // uniquePaths.length > DISTINCT_LIMIT ||
throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range."); // uniqueResources.length > DISTINCT_LIMIT
} // ) {
// throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
// }
return { return {
actors: uniqueActors actors: uniqueActors

View File

@@ -62,7 +62,26 @@ export async function exchangeSession(
cleanHost = cleanHost.slice(0, -1 * matched.length); cleanHost = cleanHost.slice(0, -1 * matched.length);
} }
const clientIp = requestIp?.split(":")[0]; const clientIp = requestIp
? (() => {
if (requestIp.startsWith("[") && requestIp.includes("]")) {
const ipv6Match = requestIp.match(/\[(.*?)\]/);
if (ipv6Match) {
return ipv6Match[1];
}
}
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}/;
if (ipv4Pattern.test(requestIp)) {
const lastColonIndex = requestIp.lastIndexOf(":");
if (lastColonIndex !== -1) {
return requestIp.substring(0, lastColonIndex);
}
}
return requestIp;
})()
: undefined;
const [resource] = await db const [resource] = await db
.select() .select()

View File

@@ -13,7 +13,8 @@ import {
LoginPage, LoginPage,
Org, Org,
Resource, Resource,
ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility, ResourceHeaderAuth,
ResourceHeaderAuthExtendedCompatibility,
ResourcePassword, ResourcePassword,
ResourcePincode, ResourcePincode,
ResourceRule, ResourceRule,
@@ -39,6 +40,8 @@ import {
} from "#dynamic/lib/checkOrgAccessPolicy"; } from "#dynamic/lib/checkOrgAccessPolicy";
import { logRequestAudit } from "./logRequestAudit"; import { logRequestAudit } from "./logRequestAudit";
import cache from "@server/lib/cache"; import cache from "@server/lib/cache";
import semver from "semver";
import { APP_VERSION } from "@server/lib/consts";
const verifyResourceSessionSchema = z.object({ const verifyResourceSessionSchema = z.object({
sessions: z.record(z.string(), z.string()).optional(), sessions: z.record(z.string(), z.string()).optional(),
@@ -50,7 +53,8 @@ const verifyResourceSessionSchema = z.object({
path: z.string(), path: z.string(),
method: z.string(), method: z.string(),
tls: z.boolean(), tls: z.boolean(),
requestIp: z.string().optional() requestIp: z.string().optional(),
badgerVersion: z.string().optional()
}); });
export type VerifyResourceSessionSchema = z.infer< export type VerifyResourceSessionSchema = z.infer<
@@ -69,6 +73,7 @@ export type VerifyUserResponse = {
headerAuthChallenged?: boolean; headerAuthChallenged?: boolean;
redirectUrl?: string; redirectUrl?: string;
userData?: BasicUserData; userData?: BasicUserData;
pangolinVersion?: string;
}; };
export async function verifyResourceSession( export async function verifyResourceSession(
@@ -97,7 +102,8 @@ export async function verifyResourceSession(
requestIp, requestIp,
path, path,
headers, headers,
query query,
badgerVersion
} = parsedBody.data; } = parsedBody.data;
// Extract HTTP Basic Auth credentials if present // Extract HTTP Basic Auth credentials if present
@@ -105,7 +111,15 @@ export async function verifyResourceSession(
const clientIp = requestIp const clientIp = requestIp
? (() => { ? (() => {
logger.debug("Request IP:", { requestIp }); const isNewerBadger =
badgerVersion &&
semver.valid(badgerVersion) &&
semver.gte(badgerVersion, "1.3.1");
if (isNewerBadger) {
return requestIp;
}
if (requestIp.startsWith("[") && requestIp.includes("]")) { if (requestIp.startsWith("[") && requestIp.includes("]")) {
// if brackets are found, extract the IPv6 address from between the brackets // if brackets are found, extract the IPv6 address from between the brackets
const ipv6Match = requestIp.match(/\[(.*?)\]/); const ipv6Match = requestIp.match(/\[(.*?)\]/);
@@ -114,12 +128,17 @@ export async function verifyResourceSession(
} }
} }
// ivp4 // Check if it looks like IPv4 (contains dots and matches IPv4 pattern)
// split at last colon // IPv4 format: x.x.x.x where x is 0-255
const lastColonIndex = requestIp.lastIndexOf(":"); const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}/;
if (lastColonIndex !== -1) { if (ipv4Pattern.test(requestIp)) {
return requestIp.substring(0, lastColonIndex); const lastColonIndex = requestIp.lastIndexOf(":");
if (lastColonIndex !== -1) {
return requestIp.substring(0, lastColonIndex);
}
} }
// Return as is
return requestIp; return requestIp;
})() })()
: undefined; : undefined;
@@ -130,9 +149,7 @@ export async function verifyResourceSession(
? await getCountryCodeFromIp(clientIp) ? await getCountryCodeFromIp(clientIp)
: undefined; : undefined;
const ipAsn = clientIp const ipAsn = clientIp ? await getAsnFromIp(clientIp) : undefined;
? await getAsnFromIp(clientIp)
: undefined;
let cleanHost = host; let cleanHost = host;
// if the host ends with :port, strip it // if the host ends with :port, strip it
@@ -178,7 +195,13 @@ export async function verifyResourceSession(
cache.set(resourceCacheKey, resourceData, 5); cache.set(resourceCacheKey, resourceData, 5);
} }
const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData; const {
resource,
pincode,
password,
headerAuth,
headerAuthExtendedCompatibility
} = resourceData;
if (!resource) { if (!resource) {
logger.debug(`Resource not found ${cleanHost}`); logger.debug(`Resource not found ${cleanHost}`);
@@ -474,8 +497,7 @@ export async function verifyResourceSession(
return notAllowed(res); return notAllowed(res);
} }
} } else if (headerAuth) {
else if (headerAuth) {
// if there are no other auth methods we need to return unauthorized if nothing is provided // if there are no other auth methods we need to return unauthorized if nothing is provided
if ( if (
!sso && !sso &&
@@ -713,7 +735,11 @@ export async function verifyResourceSession(
} }
// If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge // If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge
if (headerAuthExtendedCompatibility && headerAuthExtendedCompatibility.extendedCompatibilityIsActivated && !clientHeaderAuth){ if (
headerAuthExtendedCompatibility &&
headerAuthExtendedCompatibility.extendedCompatibilityIsActivated &&
!clientHeaderAuth
) {
return headerAuthChallenged(res, redirectPath, resource.orgId); return headerAuthChallenged(res, redirectPath, resource.orgId);
} }
@@ -825,7 +851,7 @@ async function notAllowed(
} }
const data = { const data = {
data: { valid: false, redirectUrl }, data: { valid: false, redirectUrl, pangolinVersion: APP_VERSION },
success: true, success: true,
error: false, error: false,
message: "Access denied", message: "Access denied",
@@ -839,8 +865,8 @@ function allowed(res: Response, userData?: BasicUserData) {
const data = { const data = {
data: data:
userData !== undefined && userData !== null userData !== undefined && userData !== null
? { valid: true, ...userData } ? { valid: true, ...userData, pangolinVersion: APP_VERSION }
: { valid: true }, : { valid: true, pangolinVersion: APP_VERSION },
success: true, success: true,
error: false, error: false,
message: "Access allowed", message: "Access allowed",
@@ -879,7 +905,12 @@ async function headerAuthChallenged(
} }
const data = { const data = {
data: { headerAuthChallenged: true, valid: false, redirectUrl }, data: {
headerAuthChallenged: true,
valid: false,
redirectUrl,
pangolinVersion: APP_VERSION
},
success: true, success: true,
error: false, error: false,
message: "Access denied", message: "Access denied",

View File

@@ -56,12 +56,12 @@ async function getLatestOlmVersion(): Promise<string | null> {
return null; return null;
} }
const 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 Olm repository"); logger.warn("No tags found for Olm repository");
return null; return null;
} }
tags = tags.filter((version) => !version.name.includes("rc"));
const latestVersion = tags[0].name; const latestVersion = tags[0].name;
olmVersionCache.set("latestOlmVersion", latestVersion); olmVersionCache.set("latestOlmVersion", latestVersion);

View File

@@ -39,12 +39,12 @@ async function getLatestNewtVersion(): Promise<string | null> {
return null; return null;
} }
const 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 null;
} }
tags = tags.filter((version) => !version.name.includes("rc"));
const latestVersion = tags[0].name; const latestVersion = tags[0].name;
cache.set("latestNewtVersion", latestVersion); cache.set("latestNewtVersion", latestVersion);

View File

@@ -16,7 +16,6 @@ import { SwitchInput } from "@app/components/SwitchInput";
import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Tag, TagInput } from "@app/components/tags/tag-input";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { CheckboxWithLabel } from "@app/components/ui/checkbox";
import { import {
Form, Form,
FormControl, FormControl,
@@ -184,9 +183,6 @@ export default function ResourceAuthenticationPage() {
const [ssoEnabled, setSsoEnabled] = useState(resource.sso); const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
const [autoLoginEnabled, setAutoLoginEnabled] = useState(
resource.skipToIdpId !== null && resource.skipToIdpId !== undefined
);
const [selectedIdpId, setSelectedIdpId] = useState<number | null>( const [selectedIdpId, setSelectedIdpId] = useState<number | null>(
resource.skipToIdpId || null resource.skipToIdpId || null
); );
@@ -243,17 +239,12 @@ export default function ResourceAuthenticationPage() {
text: w.email text: w.email
})) }))
); );
if (autoLoginEnabled && !selectedIdpId && orgIdps.length > 0) {
setSelectedIdpId(orgIdps[0].idpId);
}
hasInitializedRef.current = true; hasInitializedRef.current = true;
}, [ }, [
pageLoading, pageLoading,
resourceRoles, resourceRoles,
resourceUsers, resourceUsers,
whitelist, whitelist,
autoLoginEnabled,
selectedIdpId,
orgIdps orgIdps
]); ]);
@@ -269,16 +260,6 @@ export default function ResourceAuthenticationPage() {
const data = usersRolesForm.getValues(); const data = usersRolesForm.getValues();
try { try {
// Validate that an IDP is selected if auto login is enabled
if (autoLoginEnabled && !selectedIdpId) {
toast({
variant: "destructive",
title: t("error"),
description: t("selectIdpRequired")
});
return;
}
const jobs = [ const jobs = [
api.post(`/resource/${resource.resourceId}/roles`, { api.post(`/resource/${resource.resourceId}/roles`, {
roleIds: data.roles.map((i) => parseInt(i.id)) roleIds: data.roles.map((i) => parseInt(i.id))
@@ -288,7 +269,7 @@ export default function ResourceAuthenticationPage() {
}), }),
api.post(`/resource/${resource.resourceId}`, { api.post(`/resource/${resource.resourceId}`, {
sso: ssoEnabled, sso: ssoEnabled,
skipToIdpId: autoLoginEnabled ? selectedIdpId : null skipToIdpId: selectedIdpId
}) })
]; ];
@@ -296,7 +277,7 @@ export default function ResourceAuthenticationPage() {
updateResource({ updateResource({
sso: ssoEnabled, sso: ssoEnabled,
skipToIdpId: autoLoginEnabled ? selectedIdpId : null skipToIdpId: selectedIdpId
}); });
updateAuthInfo({ updateAuthInfo({
@@ -619,88 +600,55 @@ export default function ResourceAuthenticationPage() {
)} )}
{ssoEnabled && allIdps.length > 0 && ( {ssoEnabled && allIdps.length > 0 && (
<> <div className="space-y-2">
<div className="space-y-2 mb-3"> <label className="text-sm font-medium">
<CheckboxWithLabel {t(
label={t( "defaultIdentityProvider"
"autoLoginExternalIdp" )}
)} </label>
checked={autoLoginEnabled} <Select
onCheckedChange={( onValueChange={(value) => {
checked if (value === "none") {
) => { setSelectedIdpId(null);
setAutoLoginEnabled( } else {
checked as boolean setSelectedIdpId(
parseInt(value)
); );
if ( }
checked && }}
allIdps.length > 0 value={
) { selectedIdpId
setSelectedIdpId( ? selectedIdpId.toString()
allIdps[0].id : "none"
); }
} else { >
setSelectedIdpId( <SelectTrigger className="w-full mt-1">
null <SelectValue
); placeholder={t(
} "selectIdpPlaceholder"
}}
/>
<p className="text-sm text-muted-foreground">
{t(
"autoLoginExternalIdpDescription"
)}
</p>
</div>
{autoLoginEnabled && (
<div className="space-y-2">
<label className="text-sm font-medium">
{t(
"defaultIdentityProvider"
)} )}
</label> />
<Select </SelectTrigger>
onValueChange={( <SelectContent>
value <SelectItem value="none">
) => {t("none")}
setSelectedIdpId( </SelectItem>
parseInt(value) {allIdps.map((idp) => (
) <SelectItem
} key={idp.id}
value={ value={idp.id.toString()}
selectedIdpId >
? selectedIdpId.toString() {idp.text}
: undefined </SelectItem>
} ))}
> </SelectContent>
<SelectTrigger className="w-full mt-1"> </Select>
<SelectValue <p className="text-sm text-muted-foreground">
placeholder={t( {t(
"selectIdpPlaceholder" "defaultIdentityProviderDescription"
)} )}
/> </p>
</SelectTrigger> </div>
<SelectContent>
{allIdps.map(
(idp) => (
<SelectItem
key={
idp.id
}
value={idp.id.toString()}
>
{
idp.text
}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
)}
</>
)} )}
</form> </form>
</Form> </Form>

View File

@@ -87,8 +87,6 @@ export default async function OrgAuthPage(props: {
redirect(env.app.dashboardUrl); redirect(env.app.dashboardUrl);
} }
console.log(user, forceLogin);
if (user && !forceLogin) { if (user && !forceLogin) {
let redirectToken: string | undefined; let redirectToken: string | undefined;
try { try {

View File

@@ -95,7 +95,7 @@ export function OrgSelectionForm() {
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t("orgAuthWhatsThis")}{" "} {t("orgAuthWhatsThis")}{" "}
<Link <Link
href="https://docs.pangolin.net/manage/identity-providers/add-an-idp" href="https://docs.pangolin.net/manage/organizations/org-id"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="underline" className="underline"

View File

@@ -41,7 +41,10 @@ export default function ProductUpdates({
const data = useQueries({ const data = useQueries({
queries: [ queries: [
productUpdatesQueries.list(env.app.notifications.product_updates), productUpdatesQueries.list(
env.app.notifications.product_updates,
env.app.version
),
productUpdatesQueries.latestVersion( productUpdatesQueries.latestVersion(
env.app.notifications.new_releases env.app.notifications.new_releases
) )
@@ -78,10 +81,10 @@ export default function ProductUpdates({
const showNewVersionPopup = Boolean( const showNewVersionPopup = Boolean(
latestVersion && latestVersion &&
valid(latestVersion) && valid(latestVersion) &&
valid(currentVersion) && valid(currentVersion) &&
ignoredVersionUpdate !== latestVersion && ignoredVersionUpdate !== latestVersion &&
gt(latestVersion, currentVersion) gt(latestVersion, currentVersion)
); );
const filteredUpdates = data.updates.filter( const filteredUpdates = data.updates.filter(

View File

@@ -41,12 +41,13 @@ export type LatestVersionResponse = {
}; };
export const productUpdatesQueries = { export const productUpdatesQueries = {
list: (enabled: boolean) => list: (enabled: boolean, version?: string) =>
queryOptions({ queryOptions({
queryKey: ["PRODUCT_UPDATES"] as const, queryKey: ["PRODUCT_UPDATES"] as const,
queryFn: async ({ signal }) => { queryFn: async ({ signal }) => {
const sp = new URLSearchParams({ const sp = new URLSearchParams({
build build,
...(version ? { version } : {})
}); });
const data = await remote.get<ResponseT<ProductUpdate[]>>( const data = await remote.get<ResponseT<ProductUpdate[]>>(
`/product-updates?${sp.toString()}`, `/product-updates?${sp.toString()}`,