Compare commits

..

13 Commits

Author SHA1 Message Date
dependabot[bot] cb0fe867ee Bump the docker-dependencies group across 1 directory with 2 updates
Bumps the docker-dependencies group with 2 updates in the / directory: docker/library/node and node.


Updates `docker/library/node` from 24-slim to 26-slim

Updates `node` from 24-alpine to 26-alpine

---
updated-dependencies:
- dependency-name: docker/library/node
  dependency-version: 26-slim
  dependency-type: direct:production
  dependency-group: docker-dependencies
- dependency-name: node
  dependency-version: 26-alpine
  dependency-type: direct:production
  dependency-group: docker-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 01:33:06 +00:00
Milo Schwartz d4138e2141 Merge pull request #3421 from fosrl/dev
1.20.0
2026-07-09 10:40:35 -04:00
Milo Schwartz 09d5d9082e Merge pull request #3413 from fosrl/dev
1.20.0
2026-07-08 15:30:03 -04:00
Owen Schwartz 5cb316f4e9 Merge pull request #3404 from fosrl/dev
Add ? to support undefined
2026-07-07 10:59:56 -04:00
Owen Schwartz 6b6c9cf4d8 Merge pull request #3403 from fosrl/dev
Move jit mode to a config param
2026-07-07 10:41:59 -04:00
Owen Schwartz 05617c63c0 Merge pull request #3402 from fosrl/dev
1.19.4-s.5
2026-07-07 10:21:57 -04:00
Owen Schwartz d4c52bbf2f Merge pull request #3396 from fosrl/dev
1.19.4-s.5
2026-07-03 16:15:18 -04:00
Owen Schwartz 87f50bf0cc Merge pull request #3389 from fosrl/dev
1.19.4-s.3
2026-07-03 10:29:15 -04:00
Owen Schwartz 2c66da1b19 Merge pull request #3386 from v1rusnl/main
Upgrade Traefik image to version 3.7
2026-07-03 10:18:44 -04:00
v1rusnl ab19955502 Upgrade Traefik image to version 3.7 2026-07-03 08:22:13 +02:00
v1rusnl 1db9dcec81 Update Traefik image version to v3.7 2026-07-03 08:21:12 +02:00
Owen Schwartz 49c2d3163e Merge pull request #3381 from fosrl/dev
dev
2026-07-02 10:56:39 -04:00
Owen Schwartz 45b9e13a13 Merge pull request #3378 from fosrl/dev
1.19.4-s.1
2026-07-01 21:48:01 -04:00
43 changed files with 134 additions and 478 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# FROM node:24-slim AS base
FROM public.ecr.aws/docker/library/node:24-slim AS base
FROM public.ecr.aws/docker/library/node:26-slim AS base
WORKDIR /app
@@ -33,7 +33,7 @@ FROM base AS builder
RUN npm ci --omit=dev
# FROM node:24-slim AS runner
FROM public.ecr.aws/docker/library/node:24-slim AS runner
FROM public.ecr.aws/docker/library/node:26-slim AS runner
WORKDIR /app
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:24-alpine
FROM node:26-alpine
WORKDIR /app
+1 -1
View File
@@ -41,7 +41,7 @@ services:
- 80:80 # Port for traefik because of the network_mode
traefik:
image: traefik:v3.6
image: traefik:v3.7
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
+1 -1
View File
@@ -50,7 +50,7 @@ services:
- 80:80{{end}}
traefik:
image: docker.io/traefik:v3.6
image: docker.io/traefik:v3.7
container_name: traefik
restart: unless-stopped
{{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
-1
View File
@@ -1397,7 +1397,6 @@
"createOrgUser": "Create Org User",
"actionUpdateOrg": "Update Organization",
"actionRemoveInvitation": "Remove Invitation",
"actionRemoveUserRole": "Remove User Role",
"actionUpdateUser": "Update User",
"actionGetUser": "Get User",
"actionGetOrgUser": "Get Organization User",
+2 -8
View File
@@ -200,10 +200,7 @@ export const resources = pgTable(
authDaemonMode: varchar("authDaemonMode", { length: 32 })
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123),
status: varchar("status")
.$type<"pending" | "approved">()
.default("approved")
authDaemonPort: integer("authDaemonPort").default(22123)
},
(t) => [
index("idx_resources_fulldomain")
@@ -454,10 +451,7 @@ export const siteResources = pgTable(
onDelete: "set null"
}),
subdomain: varchar("subdomain"),
fullDomain: varchar("fullDomain"),
status: varchar("status")
.$type<"pending" | "approved">()
.default("approved")
fullDomain: varchar("fullDomain")
},
(t) => [index("idx_siteresources_orgid_niceid").on(t.orgId, t.niceId)]
);
+2 -4
View File
@@ -209,8 +209,7 @@ export const resources = sqliteTable("resources", {
authDaemonMode: text("authDaemonMode")
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123),
status: text("status").$type<"pending" | "approved">().default("approved")
authDaemonPort: integer("authDaemonPort").default(22123)
});
export const labels = sqliteTable("labels", {
@@ -448,8 +447,7 @@ export const siteResources = sqliteTable("siteResources", {
onDelete: "set null"
}),
subdomain: text("subdomain"),
fullDomain: text("fullDomain"),
status: text("status").$type<"pending" | "approved">().default("approved")
fullDomain: text("fullDomain")
});
export const networks = sqliteTable("networks", {
+6
View File
@@ -34,6 +34,12 @@ import {
rebuildClientAssociationsFromSiteResource,
waitForSiteResourceRebuildIdle
} from "../rebuildClientAssociations";
import { build } from "@server/build";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import next from "next";
import { LimitId } from "../billing";
import { usageService } from "../billing/usageService";
type ApplyBlueprintArgs = {
orgId: string;
+10 -16
View File
@@ -26,6 +26,9 @@ import { createCertificate } from "#dynamic/routers/certificates/createCertifica
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
import { build } from "@server/build";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import next from "next";
import { LimitId } from "../billing";
import { usageService } from "../billing/usageService";
@@ -198,19 +201,17 @@ export async function updatePrivateResources(
}
}
let resourceStatusFromSite: "approved" | "pending" = "approved";
if (siteId && allSites.length === 0) {
// only add if there are not provided sites
// Use the provided siteId directly, but verify it belongs to the org
const [siteSingle] = await trx
.select({ siteId: sites.siteId, status: sites.status })
.select({ siteId: sites.siteId })
.from(sites)
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
.limit(1);
if (siteSingle) {
allSites.push(siteSingle);
}
resourceStatusFromSite = siteSingle.status ?? "approved";
}
if (allSites.length === 0) {
@@ -219,13 +220,6 @@ export async function updatePrivateResources(
);
}
const resourceEnabled =
resourceData.enabled == undefined || resourceData.enabled == null
? true
: resourceStatusFromSite === "pending"
? false
: resourceData.enabled;
if (existingResource) {
let domainInfo:
| { subdomain: string | null; domainId: string }
@@ -249,7 +243,8 @@ export async function updatePrivateResources(
scheme: resourceData.scheme,
destination: resourceData.destination,
destinationPort: resourceData["destination-port"],
enabled: resourceEnabled,
enabled: true, // hardcoded for now
// enabled: resourceData.enabled ?? true,
alias: resourceData.alias || null,
disableIcmp:
resourceData["disable-icmp"] ||
@@ -268,8 +263,7 @@ export async function updatePrivateResources(
pamMode: resourceData["auth-daemon"]?.pam || "passthrough",
authDaemonMode:
resourceData["auth-daemon"]?.mode || "native",
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
status: resourceStatusFromSite
authDaemonPort: resourceData["auth-daemon"]?.port || 22123
})
.where(
eq(
@@ -502,7 +496,8 @@ export async function updatePrivateResources(
scheme: resourceData.scheme,
destination: resourceData.destination,
destinationPort: resourceData["destination-port"],
enabled: resourceEnabled,
enabled: true, // hardcoded for now
// enabled: resourceData.enabled ?? true,
alias: resourceData.alias || null,
aliasAddress: aliasAddress,
disableIcmp:
@@ -522,8 +517,7 @@ export async function updatePrivateResources(
pamMode: resourceData["auth-daemon"]?.pam || "passthrough",
authDaemonMode:
resourceData["auth-daemon"]?.mode || "native",
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
status: resourceStatusFromSite
authDaemonPort: resourceData["auth-daemon"]?.port || 22123
})
.returning();
+15 -37
View File
@@ -25,7 +25,6 @@ import {
rolePolicies,
roleResources,
roles,
Site,
sites,
Target,
TargetHealthCheck,
@@ -75,40 +74,19 @@ export async function updatePublicResources(
)) {
const targetsToUpdate: Target[] = [];
const healthchecksToUpdate: TargetHealthCheck[] = [];
let resource: Resource;
let resourceStatusFromSite: "approved" | "pending" = "approved";
let providedSite: Partial<Site> | undefined;
if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org
[providedSite] = await trx
.select({
siteId: sites.siteId,
type: sites.type,
status: sites.status
})
.from(sites)
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
.limit(1);
resourceStatusFromSite = providedSite?.status ?? "approved";
}
async function createTarget( // reusable function to create a target
resourceId: number,
targetData: TargetData
) {
const targetSiteId = targetData.site;
let site: Partial<Site> | undefined;
let site;
if (targetSiteId) {
// Look up site by niceId
[site] = await trx
.select({
siteId: sites.siteId,
type: sites.type,
status: sites.status
})
.select({ siteId: sites.siteId, type: sites.type })
.from(sites)
.where(
and(
@@ -117,9 +95,15 @@ export async function updatePublicResources(
)
)
.limit(1);
} else if (siteId && providedSite) {
} else if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org
site = providedSite;
[site] = await trx
.select({ siteId: sites.siteId, type: sites.type })
.from(sites)
.where(
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
)
.limit(1);
} else {
throw new Error(`Target site is required`);
}
@@ -155,7 +139,7 @@ export async function updatePublicResources(
.insert(targets)
.values({
resourceId: resourceId,
siteId: site.siteId!,
siteId: site.siteId,
ip: targetData.hostname,
mode: resourceData.mode as Target["mode"],
method: targetData.method,
@@ -188,7 +172,7 @@ export async function updatePublicResources(
.insert(targetHealthCheck)
.values({
name: `${targetData.hostname}:${targetData.port}`,
siteId: site.siteId!,
siteId: site.siteId,
targetId: newTarget.targetId,
orgId: orgId,
hcEnabled: healthcheckData?.enabled || false,
@@ -246,10 +230,7 @@ export async function updatePublicResources(
const resourceEnabled =
resourceData.enabled == undefined || resourceData.enabled == null
? true
: resourceStatusFromSite === "pending"
? false
: resourceData.enabled;
: resourceData.enabled;
const resourceSsl =
resourceData.ssl == undefined || resourceData.ssl == null
? true
@@ -425,8 +406,7 @@ export async function updatePublicResources(
? (resourceData["proxy-protocol-version"] ??
1)
: 1,
resourcePolicyId: sharedPolicy.resourcePolicyId,
status: resourceStatusFromSite
resourcePolicyId: sharedPolicy.resourcePolicyId
})
.where(
eq(
@@ -610,8 +590,7 @@ export async function updatePublicResources(
authDaemonPort:
resourceData["auth-daemon"]?.port || 22123,
resourcePolicyId: null,
defaultResourcePolicyId: inlinePolicyId,
status: resourceStatusFromSite
defaultResourcePolicyId: inlinePolicyId
})
.where(
eq(
@@ -1152,7 +1131,6 @@ export async function updatePublicResources(
.values({
orgId,
niceId: resourceNiceId,
status: resourceStatusFromSite,
name: resourceData.name || "Unnamed Resource",
mode: resourceData.mode,
proxyPort: ["http", "ssh", "rdp", "vnc"].includes(
+1 -1
View File
@@ -470,7 +470,7 @@ export const PrivateResourceSchema = z
// proxyPort: z.int().positive().optional(),
"destination-port": z.int().positive().optional(),
destination: z.string().min(1).optional(),
enabled: z.boolean().default(true),
// enabled: z.boolean().default(true),
"tcp-ports": portRangeStringSchema.optional().default("*"),
"udp-ports": portRangeStringSchema.optional().default("*"),
"disable-icmp": z.boolean().optional().default(false),
-9
View File
@@ -496,7 +496,6 @@ export function generateRemoteSubnets(
): string[] {
const remoteSubnets = allSiteResources
.filter((sr) => {
if (!sr.enabled) return false;
if (!sr.destination) return false;
if (sr.mode === "cidr") {
@@ -531,7 +530,6 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
return allSiteResources
.filter(
(sr) =>
sr.enabled &&
sr.aliasAddress &&
((sr.alias && (sr.mode == "host" || sr.mode == "ssh")) ||
(sr.fullDomain && sr.mode == "http"))
@@ -664,13 +662,6 @@ export async function generateSubnetProxyTargetV2(
subnet: string | null;
}[]
): Promise<SubnetProxyTargetV2[] | undefined> {
if (!siteResource.enabled) {
logger.debug(
`Site resource ${siteResource.siteResourceId} is disabled, skipping target generation.`
);
return;
}
if (clients.length === 0) {
logger.debug(
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
+16 -30
View File
@@ -1561,19 +1561,9 @@ export async function handleMessagingForUpdatedSiteResource(
updatedSiteResource.udpPortRangeString ||
existingSiteResource.disableIcmp !==
updatedSiteResource.disableIcmp);
// Toggling enabled on/off doesn't change any of the fields above, but it
// does change whether targets/peer data should exist at all, so it needs
// to drive the same old->new diff machinery: going enabled->disabled
// diffs "real data" against "nothing" (a remove), and disabled->enabled
// diffs "nothing" against "real data" (an add). generateSubnetProxyTargetV2/
// generateRemoteSubnets/generateAliasConfig already return nothing for a
// disabled resource, so no other changes are needed here.
const enabledChanged =
existingSiteResource &&
existingSiteResource.enabled !== updatedSiteResource.enabled;
logger.debug(
`handleMessagingForUpdatedSiteResource: change flags destinationChanged=${Boolean(destinationChanged)} destinationPortChanged=${Boolean(destinationPortChanged)} aliasChanged=${Boolean(aliasChanged)} fullDomainChanged=${Boolean(fullDomainChanged)} sslChanged=${Boolean(sslChanged)} portRangesChanged=${Boolean(portRangesChanged)} enabledChanged=${Boolean(enabledChanged)}`
`handleMessagingForUpdatedSiteResource: change flags destinationChanged=${Boolean(destinationChanged)} destinationPortChanged=${Boolean(destinationPortChanged)} aliasChanged=${Boolean(aliasChanged)} fullDomainChanged=${Boolean(fullDomainChanged)} sslChanged=${Boolean(sslChanged)} portRangesChanged=${Boolean(portRangesChanged)}`
);
// if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all
@@ -1584,16 +1574,14 @@ export async function handleMessagingForUpdatedSiteResource(
fullDomainChanged ||
sslChanged ||
portRangesChanged ||
destinationPortChanged ||
enabledChanged
destinationPortChanged
) {
const shouldUpdateTargets =
destinationChanged ||
sslChanged ||
portRangesChanged ||
fullDomainChanged ||
destinationPortChanged ||
enabledChanged;
destinationPortChanged;
logger.debug(
`handleMessagingForUpdatedSiteResource: entering unchanged-site update path shouldUpdateTargets=${shouldUpdateTargets}`
@@ -1669,22 +1657,20 @@ export async function handleMessagingForUpdatedSiteResource(
peerDataUpdateBatch.push({
clientId: client.clientId,
siteId,
remoteSubnets:
destinationChanged || enabledChanged
? {
oldRemoteSubnets:
!oldDestinationStillInUseBySite
? generateRemoteSubnets([
existingSiteResource
])
: [],
newRemoteSubnets: generateRemoteSubnets([
updatedSiteResource
])
}
: undefined,
remoteSubnets: destinationChanged
? {
oldRemoteSubnets: !oldDestinationStillInUseBySite
? generateRemoteSubnets([
existingSiteResource
])
: [],
newRemoteSubnets: generateRemoteSubnets([
updatedSiteResource
])
}
: undefined,
aliases:
aliasChanged || fullDomainChanged || enabledChanged // the full domain is sent down as an alias
aliasChanged || fullDomainChanged // the full domain is sent down as an alias
? {
oldAliases: generateAliasConfig([
existingSiteResource
@@ -157,8 +157,7 @@ async function resolveAccessibleIdsUncached(
.where(
and(
eq(userResources.userId, userId),
eq(resources.orgId, orgId),
eq(resources.status, "approved")
eq(resources.orgId, orgId)
)
),
userRoleIds.length > 0
@@ -172,8 +171,7 @@ async function resolveAccessibleIdsUncached(
.where(
and(
inArray(roleResources.roleId, userRoleIds),
eq(resources.orgId, orgId),
eq(resources.status, "approved")
eq(resources.orgId, orgId)
)
)
: Promise.resolve([]),
@@ -185,11 +183,7 @@ async function resolveAccessibleIdsUncached(
eq(effectiveResourcePolicyId, userPolicies.resourcePolicyId)
)
.where(
and(
eq(userPolicies.userId, userId),
eq(resources.orgId, orgId),
eq(resources.status, "approved")
)
and(eq(userPolicies.userId, userId), eq(resources.orgId, orgId))
),
userRoleIds.length > 0
? db
@@ -205,48 +199,21 @@ async function resolveAccessibleIdsUncached(
.where(
and(
inArray(rolePolicies.roleId, userRoleIds),
eq(resources.orgId, orgId),
eq(resources.status, "approved")
eq(resources.orgId, orgId)
)
)
: Promise.resolve([]),
db
.select({ siteResourceId: userSiteResources.siteResourceId })
.from(userSiteResources)
.innerJoin(
siteResources,
eq(
userSiteResources.siteResourceId,
siteResources.siteResourceId
)
)
.where(
and(
eq(userSiteResources.userId, userId),
eq(siteResources.orgId, orgId),
eq(siteResources.status, "approved")
)
),
.where(eq(userSiteResources.userId, userId)),
userRoleIds.length > 0
? db
.select({
siteResourceId: roleSiteResources.siteResourceId
})
.from(roleSiteResources)
.innerJoin(
siteResources,
eq(
roleSiteResources.siteResourceId,
siteResources.siteResourceId
)
)
.where(
and(
inArray(roleSiteResources.roleId, userRoleIds),
eq(siteResources.orgId, orgId),
eq(siteResources.status, "approved")
)
)
.where(inArray(roleSiteResources.roleId, userRoleIds))
: Promise.resolve([])
]);
@@ -398,7 +365,6 @@ async function filterPublicResourceIdsByTextSearch(
inArray(resources.resourceId, resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved"),
textMatch
)
);
@@ -436,7 +402,6 @@ async function filterSiteResourceIdsByTextSearch(
inArray(siteResources.siteResourceId, siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved"),
textMatch
)
);
@@ -538,8 +503,7 @@ async function listSiteGroups(
const publicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved")
eq(resources.enabled, true)
];
if (searchPublic) {
publicConditions.push(searchPublic);
@@ -594,8 +558,7 @@ async function listSiteGroups(
const siteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
eq(siteResources.enabled, true)
];
if (searchSite) {
siteConditions.push(searchSite);
@@ -658,8 +621,7 @@ async function listSiteGroups(
const noSitePublicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved")
eq(resources.enabled, true)
];
if (searchPublic) {
noSitePublicConditions.push(searchPublic);
@@ -693,8 +655,7 @@ async function listSiteGroups(
const noSiteSiteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
eq(siteResources.enabled, true)
];
if (searchSite) {
noSiteSiteConditions.push(searchSite);
@@ -785,8 +746,7 @@ async function listLabelGroups(
const publicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved")
eq(resources.enabled, true)
];
const searchPublic = buildSearchConditionForPublic(query.query);
if (searchPublic) {
@@ -850,8 +810,7 @@ async function listLabelGroups(
const siteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
eq(siteResources.enabled, true)
];
const searchSite = buildSearchConditionForSiteResource(query.query);
if (searchSite) {
@@ -1038,7 +997,6 @@ async function mapPublicResources(
inArray(resources.resourceId, resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved"),
siteIdFilter != null
? eq(sites.siteId, siteIdFilter)
: undefined
@@ -1130,7 +1088,6 @@ async function mapSiteResources(
inArray(siteResources.siteResourceId, siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved"),
siteIdFilter != null
? eq(sites.siteId, siteIdFilter)
: undefined
@@ -1425,8 +1382,7 @@ async function collectAccessibleSites(
const publicConditions = [
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved")
eq(resources.enabled, true)
];
if (siteNameSearch) {
publicConditions.push(siteNameSearch);
@@ -1466,8 +1422,7 @@ async function collectAccessibleSites(
const siteConditions = [
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved")
eq(siteResources.enabled, true)
];
if (siteNameSearch) {
siteConditions.push(siteNameSearch);
@@ -1521,7 +1476,6 @@ async function collectAccessibleLabels(
inArray(resources.resourceId, accessible.resourceIds),
eq(resources.orgId, orgId),
eq(resources.enabled, true),
eq(resources.status, "approved"),
eq(labels.orgId, orgId)
];
if (labelNameSearch) {
@@ -1557,7 +1511,6 @@ async function collectAccessibleLabels(
inArray(siteResources.siteResourceId, accessible.siteResourceIds),
eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true),
eq(siteResources.status, "approved"),
eq(labels.orgId, orgId)
];
if (labelNameSearch) {
+1 -6
View File
@@ -148,12 +148,7 @@ export async function buildClientConfigurationForNewtClient(
.from(siteResources)
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
.where(
and(
eq(siteNetworks.siteId, siteId),
eq(siteResources.enabled, true)
)
)
.where(eq(siteNetworks.siteId, siteId))
.then((rows) => rows.map((r) => r.siteResources));
const targetsToSend: SubnetProxyTargetV2[] = [];
+2 -8
View File
@@ -16,7 +16,7 @@ import {
generateRemoteSubnets
} from "@server/lib/ip";
import logger from "@server/logger";
import { and, eq, inArray } from "drizzle-orm";
import { eq, inArray } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers";
import config from "@server/lib/config";
@@ -70,13 +70,7 @@ export async function buildSiteConfigurationForOlmClient(
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
.where(
and(
eq(
clientSiteResourcesAssociationsCache.clientId,
client.clientId
),
eq(siteResources.enabled, true)
)
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
);
const siteResourcesBySiteId = new Map<number, SiteResource[]>();
-14
View File
@@ -138,15 +138,6 @@ const listResourcesSchema = z.strictObject({
description:
"When set, only resources that have at least one target on this site are returned"
}),
status: z
.enum(["pending", "approved"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["pending", "approved"],
description: "Filter by resource status"
}),
labels: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
@@ -460,7 +451,6 @@ export async function listResources(
sort_by,
order,
siteId,
status,
labels: labelFilter
} = parsedQuery.data;
@@ -670,10 +660,6 @@ export async function listResources(
}
}
if (typeof status !== "undefined") {
conditions.push(eq(resources.status, status));
}
if (siteId != null) {
const resourcesWithSite = db
.select({ resourceId: targets.resourceId })
@@ -45,19 +45,18 @@ function userResourceAliasesCacheKey(
page: number,
pageSize: number,
includeLabels: boolean,
labelFilter: string[],
status?: "pending" | "approved"
labelFilter: string[]
) {
const labelsKey =
labelFilter.length > 0 ? labelFilter.slice().sort().join(",") : "all";
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}:${includeLabels ? "labels" : "plain"}:${labelsKey}:${status ?? "all"}`;
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}:${includeLabels ? "labels" : "plain"}:${labelsKey}`;
}
const listUserResourceAliasesParamsSchema = z.strictObject({
orgId: z.string()
});
const listUserResourceAliasesQuerySchema = z.object({
const listUserResourceAliasesQuerySchema = z.strictObject({
pageSize: z.coerce
.number<string>()
.int()
@@ -97,16 +96,7 @@ const listUserResourceAliasesQuerySchema = z.object({
type: "array",
description:
"Filter by resource labels. A resource matches when it has any of the given labels (OR)."
}),
status: z
.enum(["pending", "approved"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["pending", "approved"],
description: "Filter by site resource status"
})
})
});
export type UserResourceAliasItem = {
@@ -140,8 +130,7 @@ export async function listUserResourceAliases(
page,
pageSize,
includeLabels,
labels: labelFilter,
status
labels: labelFilter
} = parsedQuery.data;
const parsedParams = listUserResourceAliasesParamsSchema.safeParse(
@@ -183,8 +172,7 @@ export async function listUserResourceAliases(
page,
pageSize,
includeLabels,
labelFilter ?? [],
status
labelFilter ?? []
);
const cachedData: ListUserResourceAliasesResponse | undefined =
await cache.get(cacheKey);
@@ -269,10 +257,6 @@ export async function listUserResourceAliases(
inArray(siteResources.siteResourceId, accessibleSiteResourceIds)
];
if (typeof status !== "undefined") {
whereConditions.push(eq(siteResources.status, status));
}
if (labelFilter && labelFilter.length > 0) {
whereConditions.push(
inArray(
@@ -56,6 +56,7 @@ const createSiteResourceSchema = z
siteId: z.number().int().positive().optional(), // DEPRECATED: for backward compatibility, we will convert this to siteIds array if provided
destinationPort: z.int().positive().optional(),
destination: z.string().min(1).nullish(),
enabled: z.boolean().default(true),
alias: z
.string()
.regex(
@@ -274,6 +275,7 @@ export async function createSiteResource(
scheme,
destinationPort,
destination,
enabled,
ssl,
alias,
userIds,
@@ -537,6 +539,7 @@ export async function createSiteResource(
destination: destination, // the ssh can be null
scheme,
destinationPort,
enabled,
alias: alias ? alias.trim() : null,
aliasAddress,
tcpPortRangeString: tcpPortRangeStringAdjusted,
@@ -86,15 +86,6 @@ const listAllSiteResourcesByOrgQuerySchema = z.strictObject({
description:
"When set, only site resources associated with this site (via network) are returned"
}),
status: z
.enum(["pending", "approved"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["pending", "approved"],
description: "Filter by site resource status"
}),
labels: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
@@ -292,7 +283,6 @@ export async function listAllSiteResourcesByOrg(
sort_by,
order,
siteId,
status,
labels: labelFilter
} = parsedQuery.data;
@@ -325,10 +315,6 @@ export async function listAllSiteResourcesByOrg(
conditions.push(eq(siteResources.mode, mode));
}
if (typeof status !== "undefined") {
conditions.push(eq(siteResources.status, status));
}
if (labelFilter && labelFilter.length > 0) {
conditions.push(
inArray(
@@ -47,15 +47,6 @@ const listSiteResourcesQuerySchema = z.strictObject({
enum: ["asc", "desc"],
default: "asc",
description: "Sort order"
}),
status: z
.enum(["pending", "approved"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["pending", "approved"],
description: "Filter by site resource status"
})
});
@@ -119,7 +110,7 @@ export async function listSiteResources(
}
const { siteId, orgId } = parsedParams.data;
const { limit, offset, sort_by, order, status } = parsedQuery.data;
const { limit, offset, sort_by, order } = parsedQuery.data;
// Verify the site exists and belongs to the org
const site = await db
@@ -133,15 +124,6 @@ export async function listSiteResources(
}
// Get site resources by joining networks to siteResources via siteNetworks
const conditions = [
eq(siteNetworks.siteId, siteId),
eq(siteResources.orgId, orgId)
];
if (typeof status !== "undefined") {
conditions.push(eq(siteResources.status, status));
}
const siteResourcesList = await db
.select()
.from(siteNetworks)
@@ -150,7 +132,12 @@ export async function listSiteResources(
siteResources,
eq(siteResources.networkId, networks.networkId)
)
.where(and(...conditions))
.where(
and(
eq(siteNetworks.siteId, siteId),
eq(siteResources.orgId, orgId)
)
)
.orderBy(
sort_by
? order === "asc"
@@ -152,11 +152,6 @@ const updateSiteResourceSchema = z
)
.refine(
(data) => {
// this is a partial update; only enforce destination when the
// caller is actually changing mode or destination
if (data.mode === undefined && data.destination === undefined) {
return true;
}
// destination is only optional for ssh mode with native authDaemonMode
if (data.mode === "ssh" && data.authDaemonMode === "native") {
return true;
@@ -416,10 +411,8 @@ export async function updateSiteResource(
: [];
const existingSiteIds = existingSiteNetworks.map((sn) => sn.siteId);
// undefined means "leave unchanged" (partial update); only nulled out
// when the mode is explicitly being changed away from http
let fullDomain: string | null | undefined = undefined;
let finalSubdomain: string | null | undefined = undefined;
let fullDomain: string | null = null;
let finalSubdomain: string | null = null;
if (domainId) {
// Validate domain and construct full domain
const domainResult = await validateAndConstructDomain(
@@ -455,11 +448,6 @@ export async function updateSiteResource(
)
);
}
} else if (mode !== undefined && mode !== "http") {
// mode is explicitly changing away from http, so the resource
// can no longer have a domain associated with it
fullDomain = null;
finalSubdomain = null;
}
// make sure the alias is unique within the org if provided
@@ -528,28 +516,15 @@ export async function updateSiteResource(
destination: destination,
destinationPort: destinationPort,
enabled: enabled,
alias:
alias !== undefined
? alias
? alias.trim()
: null
: mode !== undefined &&
mode !== "host" &&
mode !== "ssh"
? null
: undefined,
alias: alias ? alias.trim() : null,
tcpPortRangeString: tcpPortRangeStringAdjusted,
udpPortRangeString:
mode == "http" || mode == "ssh"
? ""
: udpPortRangeString,
disableIcmp:
mode !== undefined
? disableIcmp ||
(mode == "http" || mode == "ssh"
? true
: false)
: disableIcmp,
disableIcmp ||
(mode == "http" || mode == "ssh" ? true : false),
domainId,
subdomain: finalSubdomain,
fullDomain,
@@ -23,7 +23,7 @@ import {
import { ChevronsUpDown } from "lucide-react";
import { useTranslations } from "next-intl";
import type { Control, FieldPath, FieldValues } from "react-hook-form";
import { PrivateResourceMultiSiteRoutingHelp } from "@app/components/PrivateResourceMultiSiteRoutingHelp";
import { PrivateResourceMultiSiteRoutingHelp } from "./PrivateResourceMultiSiteRoutingHelp";
type PrivateResourceSitesFieldProps<T extends FieldValues> = {
control: Control<T>;
@@ -9,10 +9,10 @@ import {
} from "@app/components/Settings";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { SshServerSettingsFields } from "@app/components/SshServerSettingsFields";
import { PrivateResourceAliasField } from "@app/components/PrivateResourceDestinationFields";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceAliasField } from "./PrivateResourceDestinationFields";
import { PrivateResourceSitesField } from "./PrivateResourceSitesField";
import { getSshUseMultiSiteTargetForm } from "./privateResourceUtils";
import { inferSshPamMode } from "@app/lib/privateResourceForm";
import { getSshUseMultiSiteTargetForm } from "@app/lib/privateResourceUtils";
import {
FormControl,
FormField,
@@ -29,7 +29,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useActionState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { PrivateResourceAccessFields } from "@app/components/PrivateResourceAccessFields";
import { PrivateResourceAccessFields } from "../../PrivateResourceAccessFields";
export default function PrivateResourceAccessPage() {
const t = useTranslations();
@@ -20,12 +20,12 @@ import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceCidrDestinationField } from "@app/components/PrivateResourceDestinationFields";
import { PrivateResourcePortRanges } from "@app/components/PrivateResourcePortRanges";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { asAnyControl, asAnySetValue } from "@app/lib/formControlUtils";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
import { PrivateResourceSitesField } from "../../PrivateResourceSitesField";
import { PrivateResourceCidrDestinationField } from "../../PrivateResourceDestinationFields";
import { PrivateResourcePortRanges } from "../../PrivateResourcePortRanges";
import { buildSelectedSitesForResource } from "../../privateResourceUtils";
import { asAnyControl, asAnySetValue } from "../../formControlUtils";
import { useSaveSiteResource } from "../../useSaveSiteResource";
export default function PrivateResourceCidrPage() {
const t = useTranslations();
@@ -16,21 +16,19 @@ import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { SwitchInput } from "@app/components/SwitchInput";
import { createGeneralFormSchema } from "@app/lib/privateResourceForm";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useActionState, useMemo } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { useSaveSiteResource } from "../../useSaveSiteResource";
export default function PrivateResourceGeneralPage() {
const t = useTranslations();
@@ -43,8 +41,7 @@ export default function PrivateResourceGeneralPage() {
resolver: zodResolver(formSchema),
defaultValues: {
name: siteResource.name,
niceId: siteResource.niceId,
enabled: siteResource.enabled
niceId: siteResource.niceId
}
});
@@ -55,8 +52,7 @@ export default function PrivateResourceGeneralPage() {
const data = form.getValues();
await save({
name: data.name,
niceId: data.niceId,
enabled: data.enabled
niceId: data.niceId
});
}, null);
@@ -80,42 +76,6 @@ export default function PrivateResourceGeneralPage() {
id="private-resource-general-form"
>
<SettingsFormGrid>
<SettingsFormCell span="full">
<FormField
control={form.control}
name="enabled"
render={() => (
<FormItem>
<FormControl>
<SwitchInput
id="enable-resource"
defaultChecked={
siteResource.enabled
}
label={t(
"resourceEnable"
)}
onCheckedChange={(
val
) =>
form.setValue(
"enabled",
val
)
}
/>
</FormControl>
<FormDescription>
{t(
"disabledResourceDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormCell>
<SettingsFormCell span="half">
<FormField
control={form.control}
@@ -20,16 +20,16 @@ import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceHostDestinationFields } from "@app/components/PrivateResourceDestinationFields";
import { PrivateResourcePortRanges } from "@app/components/PrivateResourcePortRanges";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { PrivateResourceSitesField } from "../../PrivateResourceSitesField";
import { PrivateResourceHostDestinationFields } from "../../PrivateResourceDestinationFields";
import { PrivateResourcePortRanges } from "../../PrivateResourcePortRanges";
import { buildSelectedSitesForResource } from "../../privateResourceUtils";
import {
asAnyControl,
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
} from "../../formControlUtils";
import { useSaveSiteResource } from "../../useSaveSiteResource";
export default function PrivateResourceHostPage() {
const t = useTranslations();
@@ -22,15 +22,15 @@ import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceHttpFields } from "@app/components/PrivateResourceHttpFields";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { PrivateResourceSitesField } from "../../PrivateResourceSitesField";
import { PrivateResourceHttpFields } from "../../PrivateResourceHttpFields";
import { buildSelectedSitesForResource } from "../../privateResourceUtils";
import {
asAnyControl,
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
} from "../../formControlUtils";
import { useSaveSiteResource } from "../../useSaveSiteResource";
export default function PrivateResourceHttpPage() {
const t = useTranslations();
@@ -26,15 +26,15 @@ import { useTranslations } from "next-intl";
import { useActionState, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSshFields } from "@app/components/PrivateResourceSshFields";
import type { Selectedsite } from "@app/components/site-selector";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
import { PrivateResourceSshFields } from "../../PrivateResourceSshFields";
import { buildSelectedSitesForResource } from "../../privateResourceUtils";
import {
asAnyControl,
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
import { buildSelectedSitesForResource } from "@app/lib/privateResourceUtils";
} from "../../formControlUtils";
import { useSaveSiteResource } from "../../useSaveSiteResource";
import type { Selectedsite } from "@app/components/site-selector";
export default function PrivateResourceSshPage() {
const t = useTranslations();
@@ -50,20 +50,16 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState, useTransition } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PrivateResourceSitesField } from "@app/components/PrivateResourceSitesField";
import { PrivateResourceHttpFields } from "@app/components/PrivateResourceHttpFields";
import { PrivateResourceSshFields } from "@app/components/PrivateResourceSshFields";
import { PrivateResourcePortRanges } from "@app/components/PrivateResourcePortRanges";
import { PrivateResourceSitesField } from "../PrivateResourceSitesField";
import { PrivateResourceHttpFields } from "../PrivateResourceHttpFields";
import { PrivateResourceSshFields } from "../PrivateResourceSshFields";
import { PrivateResourcePortRanges } from "../PrivateResourcePortRanges";
import {
PrivateResourceAliasField,
PrivateResourceCidrDestinationField,
PrivateResourceHostDestinationFields
} from "@app/components/PrivateResourceDestinationFields";
import {
asAnyControl,
asAnySetValue,
asAnyWatch
} from "@app/lib/formControlUtils";
} from "../PrivateResourceDestinationFields";
import { asAnyControl, asAnySetValue, asAnyWatch } from "../formControlUtils";
export default function CreatePrivateResourcePage() {
const params = useParams();
@@ -27,7 +27,6 @@ export default async function ClientResourcesPage(
const params = await props.params;
const t = await getTranslations();
const searchParams = new URLSearchParams(await props.searchParams);
searchParams.set("status", "approved");
let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = [];
let pagination: ListAllSiteResourcesByOrgResponse["pagination"] = {
@@ -38,7 +38,6 @@ export default async function ProxyResourcesPage(
const params = await props.params;
const t = await getTranslations();
const searchParams = new URLSearchParams(await props.searchParams);
searchParams.set("status", "approved");
let resources: ListResourcesResponse["resources"] = [];
let pagination: ListResourcesResponse["pagination"] = {
+1 -108
View File
@@ -23,7 +23,6 @@ import {
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import { Switch } from "@app/components/ui/switch";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useNavigationContext } from "@app/hooks/useNavigationContext";
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
@@ -37,9 +36,7 @@ import { getPrivateResourceSettingsHref } from "@app/lib/launcherResourceAdminHr
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { UpdateSiteResourceResponse } from "@server/routers/siteResource";
import type { PaginationState } from "@tanstack/react-table";
import { AxiosResponse } from "axios";
import {
ArrowDown01Icon,
ArrowRight,
@@ -52,17 +49,8 @@ import {
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
startTransition,
useMemo,
useOptimistic,
useRef,
useState,
useTransition,
type ComponentRef
} from "react";
import { startTransition, useMemo, useState, useTransition } from "react";
import { useDebouncedCallback } from "use-debounce";
import z from "zod";
import { ColumnFilterButton } from "./ColumnFilterButton";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
import { LabelsTableCell } from "./LabelsTableCell";
@@ -126,11 +114,6 @@ function isSafeUrlForLink(href: string): boolean {
}
}
const booleanSearchFilterSchema = z
.enum(["true", "false"])
.optional()
.catch(undefined);
type ClientResourcesTableProps = {
internalResources: InternalResourceRow[];
orgId: string;
@@ -191,30 +174,6 @@ export default function PrivateResourcesTable({
});
};
async function toggleInternalResourceEnabled(
val: boolean,
resourceId: number
) {
try {
await api.post<AxiosResponse<UpdateSiteResourceResponse>>(
`site-resource/${resourceId}`,
{
enabled: val
}
);
router.refresh();
} catch (e) {
toast({
variant: "destructive",
title: t("resourcesErrorUpdate"),
description: formatAxiosError(
e,
t("resourcesErrorUpdateDescription")
)
});
}
}
const deleteInternalResource = async (
resourceId: number,
siteId: number
@@ -470,36 +429,6 @@ export default function PrivateResourcesTable({
);
}
},
{
accessorKey: "enabled",
friendlyName: t("enabled"),
header: () => (
<ColumnFilterButton
options={[
{ value: "true", label: t("enabled") },
{ value: "false", label: t("disabled") }
]}
selectedValue={booleanSearchFilterSchema.parse(
searchParams.get("enabled")
)}
onValueChange={(value) =>
handleFilterChange("enabled", value)
}
searchPlaceholder={t("searchPlaceholder")}
emptyMessage={t("emptySearchOptions")}
label={t("enabled")}
className="p-3"
/>
),
cell: ({ row }) => (
<InternalResourceEnabledForm
resource={row.original}
onToggleInternalResourceEnabled={
toggleInternalResourceEnabled
}
/>
)
},
{
id: "labels",
accessorKey: "labels",
@@ -714,39 +643,3 @@ function ClientResourceLabelCell({
/>
);
}
type InternalResourceEnabledFormProps = {
resource: InternalResourceRow;
onToggleInternalResourceEnabled: (
val: boolean,
resourceId: number
) => Promise<void>;
};
function InternalResourceEnabledForm({
resource,
onToggleInternalResourceEnabled
}: InternalResourceEnabledFormProps) {
const [optimisticEnabled, setOptimisticEnabled] = useOptimistic(
resource.enabled
);
const formRef = useRef<ComponentRef<"form">>(null);
async function submitAction(formData: FormData) {
const newEnabled = !(formData.get("enabled") === "on");
setOptimisticEnabled(newEnabled);
await onToggleInternalResourceEnabled(newEnabled, resource.id);
}
return (
<form action={submitAction} ref={formRef}>
<Switch
checked={optimisticEnabled}
disabled={optimisticEnabled !== resource.enabled}
name="enabled"
onCheckedChange={() => formRef.current?.requestSubmit()}
/>
</form>
);
}
+2 -2
View File
@@ -165,6 +165,7 @@ export function buildCreateSiteResourcePayload(
siteIds: data.siteIds,
mode: data.mode,
destination: isNativeSsh ? undefined : (data.destination ?? undefined),
enabled: true,
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
@@ -341,8 +342,7 @@ export function createGeneralFormSchema(t: TranslateFn) {
.string()
.min(1)
.max(255)
.regex(/^[a-zA-Z0-9-]+$/),
enabled: z.boolean()
.regex(/^[a-zA-Z0-9-]+$/)
});
}