mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-08 17:29:54 +00:00
Compare commits
9 Commits
1.18.0
...
miloschwar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8685cf4208 | ||
|
|
26fe1259da | ||
|
|
3bcbeb24f3 | ||
|
|
1d0a92c83e | ||
|
|
a44100c2bd | ||
|
|
2203ebf723 | ||
|
|
70958185bd | ||
|
|
7e374baee9 | ||
|
|
4cf6ca1d55 |
@@ -41,7 +41,7 @@
|
|||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls.
|
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -125,12 +125,12 @@ export async function updateClientResources(
|
|||||||
|
|
||||||
const existingSiteIds = existingResource?.networkId
|
const existingSiteIds = existingResource?.networkId
|
||||||
? await trx
|
? await trx
|
||||||
.select({ siteId: sites.siteId })
|
.select({ siteId: siteNetworks.siteId })
|
||||||
.from(siteNetworks)
|
.from(siteNetworks)
|
||||||
.where(eq(siteNetworks.networkId, existingResource.networkId))
|
.where(eq(siteNetworks.networkId, existingResource.networkId))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
let allSites: { siteId: number }[] = [];
|
const allSites: { siteId: number }[] = [];
|
||||||
if (resourceData.site) {
|
if (resourceData.site) {
|
||||||
let siteSingle;
|
let siteSingle;
|
||||||
const resourceSiteId = resourceData.site;
|
const resourceSiteId = resourceData.site;
|
||||||
@@ -215,9 +215,17 @@ export async function updateClientResources(
|
|||||||
enabled: true, // hardcoded for now
|
enabled: true, // hardcoded for now
|
||||||
// enabled: resourceData.enabled ?? true,
|
// enabled: resourceData.enabled ?? true,
|
||||||
alias: resourceData.alias || null,
|
alias: resourceData.alias || null,
|
||||||
disableIcmp: resourceData["disable-icmp"],
|
disableIcmp:
|
||||||
tcpPortRangeString: resourceData["tcp-ports"],
|
resourceData["disable-icmp"] ||
|
||||||
udpPortRangeString: resourceData["udp-ports"],
|
(resourceData.mode == "http" ? true : false), // default to true for http resources, otherwise false
|
||||||
|
tcpPortRangeString:
|
||||||
|
resourceData.mode == "http"
|
||||||
|
? "443,80"
|
||||||
|
: resourceData["tcp-ports"],
|
||||||
|
udpPortRangeString:
|
||||||
|
resourceData.mode == "http"
|
||||||
|
? ""
|
||||||
|
: resourceData["udp-ports"],
|
||||||
fullDomain: resourceData["full-domain"] || null,
|
fullDomain: resourceData["full-domain"] || null,
|
||||||
subdomain: domainInfo ? domainInfo.subdomain : null,
|
subdomain: domainInfo ? domainInfo.subdomain : null,
|
||||||
domainId: domainInfo ? domainInfo.domainId : null
|
domainId: domainInfo ? domainInfo.domainId : null
|
||||||
@@ -397,9 +405,17 @@ export async function updateClientResources(
|
|||||||
// enabled: resourceData.enabled ?? true,
|
// enabled: resourceData.enabled ?? true,
|
||||||
alias: resourceData.alias || null,
|
alias: resourceData.alias || null,
|
||||||
aliasAddress: aliasAddress,
|
aliasAddress: aliasAddress,
|
||||||
disableIcmp: resourceData["disable-icmp"],
|
disableIcmp:
|
||||||
tcpPortRangeString: resourceData["tcp-ports"],
|
resourceData["disable-icmp"] ||
|
||||||
udpPortRangeString: resourceData["udp-ports"],
|
(resourceData.mode == "http" ? true : false), // default to true for http resources, otherwise false
|
||||||
|
tcpPortRangeString:
|
||||||
|
resourceData.mode == "http"
|
||||||
|
? "443,80"
|
||||||
|
: resourceData["tcp-ports"],
|
||||||
|
udpPortRangeString:
|
||||||
|
resourceData.mode == "http"
|
||||||
|
? ""
|
||||||
|
: resourceData["udp-ports"],
|
||||||
fullDomain: resourceData["full-domain"] || null,
|
fullDomain: resourceData["full-domain"] || null,
|
||||||
subdomain: domainInfo ? domainInfo.subdomain : null,
|
subdomain: domainInfo ? domainInfo.subdomain : null,
|
||||||
domainId: domainInfo ? domainInfo.domainId : null
|
domainId: domainInfo ? domainInfo.domainId : null
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, olms, users } from "@server/db";
|
import { db, idp, idpOidcConfig, olms, users } from "@server/db";
|
||||||
import { clients, currentFingerprint } from "@server/db";
|
import { clients, currentFingerprint } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -236,6 +236,9 @@ export type GetClientResponse = NonNullable<
|
|||||||
lastSeen: number | null;
|
lastSeen: number | null;
|
||||||
} | null;
|
} | null;
|
||||||
posture: PostureData | null;
|
posture: PostureData | null;
|
||||||
|
userType: string | null;
|
||||||
|
idpName: string | null;
|
||||||
|
idpVariant: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
@@ -337,6 +340,30 @@ export async function getClient(
|
|||||||
: maskPostureDataWithPlaceholder(rawPosture)
|
: maskPostureDataWithPlaceholder(rawPosture)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
let userType: string | null = null;
|
||||||
|
let idpName: string | null = null;
|
||||||
|
let idpVariant: string | null = null;
|
||||||
|
|
||||||
|
if (client.clients.userId) {
|
||||||
|
const [idpRow] = await db
|
||||||
|
.select({
|
||||||
|
userType: users.type,
|
||||||
|
idpName: idp.name,
|
||||||
|
idpVariant: idpOidcConfig.variant
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||||
|
.leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId))
|
||||||
|
.where(eq(users.userId, client.clients.userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (idpRow) {
|
||||||
|
userType = idpRow.userType;
|
||||||
|
idpName = idpRow.idpName;
|
||||||
|
idpVariant = idpRow.idpVariant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const data: GetClientResponse = {
|
const data: GetClientResponse = {
|
||||||
...client.clients,
|
...client.clients,
|
||||||
name: clientName,
|
name: clientName,
|
||||||
@@ -347,7 +374,10 @@ export async function getClient(
|
|||||||
userName: client.user?.name ?? null,
|
userName: client.user?.name ?? null,
|
||||||
userUsername: client.user?.username ?? null,
|
userUsername: client.user?.username ?? null,
|
||||||
fingerprint: fingerprintData,
|
fingerprint: fingerprintData,
|
||||||
posture: postureData
|
posture: postureData,
|
||||||
|
userType,
|
||||||
|
idpName,
|
||||||
|
idpVariant
|
||||||
};
|
};
|
||||||
|
|
||||||
return response<GetClientResponse>(res, {
|
return response<GetClientResponse>(res, {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
clients,
|
clients,
|
||||||
currentFingerprint,
|
currentFingerprint,
|
||||||
db,
|
db,
|
||||||
|
idp,
|
||||||
|
idpOidcConfig,
|
||||||
olms,
|
olms,
|
||||||
orgs,
|
orgs,
|
||||||
roleClients,
|
roleClients,
|
||||||
@@ -165,6 +167,9 @@ function queryUserDevicesBase() {
|
|||||||
userId: clients.userId,
|
userId: clients.userId,
|
||||||
username: users.username,
|
username: users.username,
|
||||||
userEmail: users.email,
|
userEmail: users.email,
|
||||||
|
userType: users.type,
|
||||||
|
idpName: idp.name,
|
||||||
|
idpVariant: idpOidcConfig.variant,
|
||||||
niceId: clients.niceId,
|
niceId: clients.niceId,
|
||||||
agent: olms.agent,
|
agent: olms.agent,
|
||||||
approvalState: clients.approvalState,
|
approvalState: clients.approvalState,
|
||||||
@@ -184,6 +189,8 @@ function queryUserDevicesBase() {
|
|||||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||||
.leftJoin(users, eq(clients.userId, users.userId))
|
.leftJoin(users, eq(clients.userId, users.userId))
|
||||||
|
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||||
|
.leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId))
|
||||||
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId));
|
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, Site, siteNetworks, siteResources } from "@server/db";
|
import { db, Site, siteNetworks, siteResources } from "@server/db";
|
||||||
import { newts, newtSessions, sites } from "@server/db";
|
import { newts, newtSessions, sites } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -77,17 +77,20 @@ export async function deleteSite(
|
|||||||
.where(eq(siteNetworks.siteId, siteId));
|
.where(eq(siteNetworks.siteId, siteId));
|
||||||
|
|
||||||
// loop through them
|
// loop through them
|
||||||
for (const network of await networks) {
|
const updatedSiteResources = await trx
|
||||||
const [siteResource] = await trx
|
.select()
|
||||||
.select()
|
.from(siteResources)
|
||||||
.from(siteResources)
|
.where(
|
||||||
.where(eq(siteResources.networkId, network.networkId));
|
inArray(
|
||||||
if (siteResource) {
|
siteResources.networkId,
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
networks.map((n) => n.networkId)
|
||||||
siteResource,
|
)
|
||||||
trx
|
);
|
||||||
);
|
for (const siteResource of updatedSiteResources) {
|
||||||
}
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
|
siteResource,
|
||||||
|
trx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the newt on the site by querying the newt table for siteId
|
// get the newt on the site by querying the newt table for siteId
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const createSiteResourceSchema = z
|
|||||||
ssl: z.boolean().optional(), // only used for http mode
|
ssl: z.boolean().optional(), // only used for http mode
|
||||||
scheme: z.enum(["http", "https"]).optional(),
|
scheme: z.enum(["http", "https"]).optional(),
|
||||||
siteIds: z.array(z.int()),
|
siteIds: z.array(z.int()),
|
||||||
|
siteId: z.number().int().positive().optional(), // DEPRECATED: for backward compatibility, we will convert this to siteIds array if provided
|
||||||
// proxyPort: z.int().positive().optional(),
|
// proxyPort: z.int().positive().optional(),
|
||||||
destinationPort: z.int().positive().optional(),
|
destinationPort: z.int().positive().optional(),
|
||||||
destination: z.string().min(1),
|
destination: z.string().min(1),
|
||||||
@@ -187,7 +188,8 @@ export async function createSiteResource(
|
|||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
niceId,
|
niceId,
|
||||||
siteIds,
|
siteIds: siteIdsInput,
|
||||||
|
siteId,
|
||||||
mode,
|
mode,
|
||||||
scheme,
|
scheme,
|
||||||
// proxyPort,
|
// proxyPort,
|
||||||
@@ -208,6 +210,12 @@ export async function createSiteResource(
|
|||||||
subdomain
|
subdomain
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
|
// Backward compatibility: merge deprecated siteId into siteIds array
|
||||||
|
const siteIds = [...siteIdsInput];
|
||||||
|
if (siteId !== undefined && !siteIds.includes(siteId)) {
|
||||||
|
siteIds.push(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
if (mode == "http") {
|
if (mode == "http") {
|
||||||
const hasHttpFeature = await isLicensedOrSubscribed(
|
const hasHttpFeature = await isLicensedOrSubscribed(
|
||||||
orgId,
|
orgId,
|
||||||
@@ -389,9 +397,10 @@ export async function createSiteResource(
|
|||||||
enabled,
|
enabled,
|
||||||
alias: alias ? alias.trim() : null,
|
alias: alias ? alias.trim() : null,
|
||||||
aliasAddress,
|
aliasAddress,
|
||||||
tcpPortRangeString,
|
tcpPortRangeString:
|
||||||
udpPortRangeString,
|
mode == "http" ? "443,80" : tcpPortRangeString,
|
||||||
disableIcmp,
|
udpPortRangeString: mode == "http" ? "" : udpPortRangeString,
|
||||||
|
disableIcmp: disableIcmp || (mode == "http" ? true : false), // default to true for http resources, otherwise false
|
||||||
domainId,
|
domainId,
|
||||||
subdomain: finalSubdomain,
|
subdomain: finalSubdomain,
|
||||||
fullDomain
|
fullDomain
|
||||||
@@ -496,7 +505,13 @@ export async function createSiteResource(
|
|||||||
`Created site resource ${newSiteResource.siteResourceId} for org ${orgId}`
|
`Created site resource ${newSiteResource.siteResourceId} for org ${orgId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ssl && mode === "http" && domainId && fullDomain && build != "oss") {
|
if (
|
||||||
|
ssl &&
|
||||||
|
mode === "http" &&
|
||||||
|
domainId &&
|
||||||
|
fullDomain &&
|
||||||
|
build != "oss"
|
||||||
|
) {
|
||||||
await createCertificate(domainId, fullDomain, db);
|
await createCertificate(domainId, fullDomain, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,9 +98,11 @@ export type ListAllSiteResourcesByOrgResponse = PaginatedResponse<{
|
|||||||
*/
|
*/
|
||||||
function aggCol<T>(column: any) {
|
function aggCol<T>(column: any) {
|
||||||
if (DB_TYPE === "sqlite") {
|
if (DB_TYPE === "sqlite") {
|
||||||
|
// json_group_array will include NULLs for left-joined missing rows;
|
||||||
|
// we filter them out in transformSiteResourceRow keeping arrays aligned.
|
||||||
return sql<T>`json_group_array(${column})`;
|
return sql<T>`json_group_array(${column})`;
|
||||||
}
|
}
|
||||||
return sql<T>`array_agg(${column})`;
|
return sql<T>`COALESCE(array_agg(${column}) FILTER (WHERE ${sites.siteId} IS NOT NULL), '{}')`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,16 +114,36 @@ function transformSiteResourceRow(row: any) {
|
|||||||
if (DB_TYPE !== "sqlite") {
|
if (DB_TYPE !== "sqlite") {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
const siteIdsRaw = JSON.parse(row.siteIds) as (number | null)[];
|
||||||
|
const siteNamesRaw = JSON.parse(row.siteNames) as (string | null)[];
|
||||||
|
const siteNiceIdsRaw = JSON.parse(row.siteNiceIds) as (string | null)[];
|
||||||
|
const siteAddressesRaw = JSON.parse(row.siteAddresses) as (string | null)[];
|
||||||
|
const siteOnlinesRaw = JSON.parse(row.siteOnlines) as (0 | 1 | null)[];
|
||||||
|
|
||||||
|
// When a site resource has no associated sites (left join produced no
|
||||||
|
// matches), the aggregated arrays will contain a single NULL entry. Strip
|
||||||
|
// those out, keeping the parallel arrays aligned by siteId presence.
|
||||||
|
const siteIds: number[] = [];
|
||||||
|
const siteNames: string[] = [];
|
||||||
|
const siteNiceIds: string[] = [];
|
||||||
|
const siteAddresses: (string | null)[] = [];
|
||||||
|
const siteOnlines: boolean[] = [];
|
||||||
|
for (let i = 0; i < siteIdsRaw.length; i++) {
|
||||||
|
if (siteIdsRaw[i] == null) continue;
|
||||||
|
siteIds.push(siteIdsRaw[i] as number);
|
||||||
|
siteNames.push((siteNamesRaw[i] ?? "") as string);
|
||||||
|
siteNiceIds.push((siteNiceIdsRaw[i] ?? "") as string);
|
||||||
|
siteAddresses.push(siteAddressesRaw[i] ?? null);
|
||||||
|
siteOnlines.push(siteOnlinesRaw[i] === 1);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
siteNames: JSON.parse(row.siteNames) as string[],
|
siteNames,
|
||||||
siteNiceIds: JSON.parse(row.siteNiceIds) as string[],
|
siteNiceIds,
|
||||||
siteIds: JSON.parse(row.siteIds) as number[],
|
siteIds,
|
||||||
siteAddresses: JSON.parse(row.siteAddresses) as (string | null)[],
|
siteAddresses,
|
||||||
// SQLite stores booleans as 0/1 integers
|
siteOnlines
|
||||||
siteOnlines: (JSON.parse(row.siteOnlines) as (0 | 1)[]).map(
|
|
||||||
(v) => v === 1
|
|
||||||
) as boolean[]
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +180,11 @@ function querySiteResourcesBase() {
|
|||||||
siteOnlines: aggCol<boolean[]>(sites.online)
|
siteOnlines: aggCol<boolean[]>(sites.online)
|
||||||
})
|
})
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.innerJoin(
|
.leftJoin(
|
||||||
siteNetworks,
|
siteNetworks,
|
||||||
eq(siteResources.networkId, siteNetworks.networkId)
|
eq(siteResources.networkId, siteNetworks.networkId)
|
||||||
)
|
)
|
||||||
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
.leftJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
||||||
.groupBy(siteResources.siteResourceId);
|
.groupBy(siteResources.siteResourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +237,8 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
const conditions = [and(eq(siteResources.orgId, orgId))];
|
const conditions = [and(eq(siteResources.orgId, orgId))];
|
||||||
|
|
||||||
if (siteId != null) {
|
if (siteId != null) {
|
||||||
|
// Keep inner joins here: filtering by a specific site implies the
|
||||||
|
// resource must have at least one matching site.
|
||||||
const resourcesForSite = db
|
const resourcesForSite = db
|
||||||
.select({ id: siteResources.siteResourceId })
|
.select({ id: siteResources.siteResourceId })
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const updateSiteResourceSchema = z
|
|||||||
.strictObject({
|
.strictObject({
|
||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
siteIds: z.array(z.int()),
|
siteIds: z.array(z.int()),
|
||||||
|
siteId: z.int().positive().optional(),
|
||||||
// niceId: z.string().min(1).max(255).regex(/^[a-zA-Z0-9-]+$/, "niceId can only contain letters, numbers, and dashes").optional(),
|
// niceId: z.string().min(1).max(255).regex(/^[a-zA-Z0-9-]+$/, "niceId can only contain letters, numbers, and dashes").optional(),
|
||||||
niceId: z
|
niceId: z
|
||||||
.string()
|
.string()
|
||||||
@@ -196,7 +197,8 @@ export async function updateSiteResource(
|
|||||||
const { siteResourceId } = parsedParams.data;
|
const { siteResourceId } = parsedParams.data;
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
siteIds, // because it can change
|
siteIds: siteIdsInput, // because it can change
|
||||||
|
siteId,
|
||||||
niceId,
|
niceId,
|
||||||
mode,
|
mode,
|
||||||
scheme,
|
scheme,
|
||||||
@@ -217,6 +219,12 @@ export async function updateSiteResource(
|
|||||||
subdomain
|
subdomain
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
|
// Backward compatibility: merge deprecated siteId into siteIds array
|
||||||
|
const siteIds = [...siteIdsInput];
|
||||||
|
if (siteId !== undefined && !siteIds.includes(siteId)) {
|
||||||
|
siteIds.push(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if site resource exists
|
// Check if site resource exists
|
||||||
const [existingSiteResource] = await db
|
const [existingSiteResource] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -440,9 +448,12 @@ export async function updateSiteResource(
|
|||||||
destinationPort,
|
destinationPort,
|
||||||
enabled,
|
enabled,
|
||||||
alias: alias ? alias.trim() : null,
|
alias: alias ? alias.trim() : null,
|
||||||
tcpPortRangeString,
|
tcpPortRangeString:
|
||||||
udpPortRangeString,
|
mode == "http" ? "443,80" : tcpPortRangeString,
|
||||||
disableIcmp,
|
udpPortRangeString:
|
||||||
|
mode == "http" ? "" : udpPortRangeString,
|
||||||
|
disableIcmp:
|
||||||
|
disableIcmp || (mode == "http" ? true : false), // default to true for http resources, otherwise false
|
||||||
domainId,
|
domainId,
|
||||||
subdomain: finalSubdomain,
|
subdomain: finalSubdomain,
|
||||||
fullDomain,
|
fullDomain,
|
||||||
@@ -734,6 +745,9 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
const fullDomainChanged =
|
const fullDomainChanged =
|
||||||
existingSiteResource &&
|
existingSiteResource &&
|
||||||
existingSiteResource.fullDomain !== updatedSiteResource.fullDomain;
|
existingSiteResource.fullDomain !== updatedSiteResource.fullDomain;
|
||||||
|
const sslChanged =
|
||||||
|
existingSiteResource &&
|
||||||
|
existingSiteResource.ssl !== updatedSiteResource.ssl;
|
||||||
const portRangesChanged =
|
const portRangesChanged =
|
||||||
existingSiteResource &&
|
existingSiteResource &&
|
||||||
(existingSiteResource.tcpPortRangeString !==
|
(existingSiteResource.tcpPortRangeString !==
|
||||||
@@ -749,6 +763,7 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
destinationChanged ||
|
destinationChanged ||
|
||||||
aliasChanged ||
|
aliasChanged ||
|
||||||
fullDomainChanged ||
|
fullDomainChanged ||
|
||||||
|
sslChanged ||
|
||||||
portRangesChanged ||
|
portRangesChanged ||
|
||||||
destinationPortChanged
|
destinationPortChanged
|
||||||
) {
|
) {
|
||||||
@@ -765,9 +780,10 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update targets on newt if destination changed
|
// Only update targets on newt if these items change
|
||||||
if (
|
if (
|
||||||
destinationChanged ||
|
destinationChanged ||
|
||||||
|
sslChanged || // we need to push a new cert if the ssl changed
|
||||||
portRangesChanged ||
|
portRangesChanged ||
|
||||||
fullDomainChanged || // if the domain changes we need to update the certs and stuff
|
fullDomainChanged || // if the domain changes we need to update the certs and stuff
|
||||||
destinationPortChanged
|
destinationPortChanged
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ export default async function ClientsPage(props: ClientsPageProps) {
|
|||||||
userId: client.userId,
|
userId: client.userId,
|
||||||
username: client.username,
|
username: client.username,
|
||||||
userEmail: client.userEmail,
|
userEmail: client.userEmail,
|
||||||
|
userType: client.userType ?? null,
|
||||||
|
idpName: client.idpName ?? null,
|
||||||
|
idpVariant: client.idpVariant ?? null,
|
||||||
niceId: client.niceId,
|
niceId: client.niceId,
|
||||||
agent: client.agent,
|
agent: client.agent,
|
||||||
archived: Boolean(client.archived),
|
archived: Boolean(client.archived),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
InfoSections,
|
InfoSections,
|
||||||
InfoSectionTitle
|
InfoSectionTitle
|
||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
@@ -36,7 +37,24 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
|
|||||||
{userDisplayName ? t("user") : t("identifier")}
|
{userDisplayName ? t("user") : t("identifier")}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{userDisplayName || client.niceId}
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span>{userDisplayName || client.niceId}</span>
|
||||||
|
{userDisplayName &&
|
||||||
|
(client.userType ?? "internal") !==
|
||||||
|
"internal" && (
|
||||||
|
<IdpTypeBadge
|
||||||
|
type={client.userType ?? "oidc"}
|
||||||
|
name={
|
||||||
|
client.idpName?.trim()
|
||||||
|
? client.idpName
|
||||||
|
: t("idpNameInternal")
|
||||||
|
}
|
||||||
|
variant={
|
||||||
|
client.idpVariant ?? undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
|
|||||||
@@ -37,11 +37,8 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Selectedsite, SitesSelector } from "@app/components/site-selector";
|
import { Selectedsite, SitesSelector } from "@app/components/site-selector";
|
||||||
import { useEffect, useMemo, useState, useTransition } from "react";
|
import { useEffect, useMemo, useState, useTransition } from "react";
|
||||||
|
|
||||||
import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog";
|
import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog";
|
||||||
import EditInternalResourceDialog from "@app/components/EditInternalResourceDialog";
|
import EditInternalResourceDialog from "@app/components/EditInternalResourceDialog";
|
||||||
import { orgQueries } from "@app/lib/queries";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import type { PaginationState } from "@tanstack/react-table";
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||||
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
@@ -206,7 +203,11 @@ export default function ClientResourcesTable({
|
|||||||
const { siteNames, siteNiceIds, orgId } = resourceRow;
|
const { siteNames, siteNiceIds, orgId } = resourceRow;
|
||||||
|
|
||||||
if (!siteNames || siteNames.length === 0) {
|
if (!siteNames || siteNames.length === 0) {
|
||||||
return <span>-</span>;
|
return (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("noSites", { defaultValue: "No sites" })}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (siteNames.length === 1) {
|
if (siteNames.length === 1) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { useMemo, useState, useTransition } from "react";
|
|||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import ClientDownloadBanner from "./ClientDownloadBanner";
|
import ClientDownloadBanner from "./ClientDownloadBanner";
|
||||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||||
|
import IdpTypeBadge from "./IdpTypeBadge";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||||
|
|
||||||
@@ -52,6 +53,9 @@ export type ClientRow = {
|
|||||||
userId: string | null;
|
userId: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
userEmail: string | null;
|
userEmail: string | null;
|
||||||
|
userType: string | null;
|
||||||
|
idpName: string | null;
|
||||||
|
idpVariant: string | null;
|
||||||
niceId: string;
|
niceId: string;
|
||||||
agent: string | null;
|
agent: string | null;
|
||||||
approvalState: "approved" | "pending" | "denied" | null;
|
approvalState: "approved" | "pending" | "denied" | null;
|
||||||
@@ -370,17 +374,30 @@ export default function UserDevicesTable({
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
return r.userId ? (
|
return r.userId ? (
|
||||||
<Link
|
<div className="flex items-center gap-2">
|
||||||
href={`/${r.orgId}/settings/access/users/${r.userId}`}
|
<Link
|
||||||
>
|
href={`/${r.orgId}/settings/access/users/${r.userId}`}
|
||||||
<Button variant="outline" size="sm">
|
>
|
||||||
{getUserDisplayName({
|
<Button variant="outline" size="sm">
|
||||||
email: r.userEmail,
|
{getUserDisplayName({
|
||||||
username: r.username
|
email: r.userEmail,
|
||||||
}) || r.userId}
|
username: r.username
|
||||||
<ArrowUpRight className="ml-2 h-3 w-3" />
|
}) || r.userId}
|
||||||
</Button>
|
<ArrowUpRight className="ml-2 h-3 w-3" />
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
{(r.userType ?? "internal") !== "internal" && (
|
||||||
|
<IdpTypeBadge
|
||||||
|
type={r.userType ?? "oidc"}
|
||||||
|
name={
|
||||||
|
r.idpName?.trim()
|
||||||
|
? r.idpName
|
||||||
|
: t("idpNameInternal")
|
||||||
|
}
|
||||||
|
variant={r.idpVariant ?? undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
"-"
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user