Compare commits

..

4 Commits

Author SHA1 Message Date
Owen
756f3f32ca Merge branch 'dev' 2026-02-17 21:57:49 -08:00
Owen
5987f6b2cd Allow enterprise 2026-02-17 21:55:57 -08:00
Owen
09a9457021 Fix transaction issue 2026-02-17 21:27:23 -08:00
Owen Schwartz
ca4643ec36 Merge pull request #2494 from fosrl/dev
1.15.4-s.3
2026-02-17 21:07:04 -08:00
15 changed files with 69 additions and 142 deletions

View File

@@ -56,41 +56,6 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download MaxMind GeoLite2 databases
env:
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
run: |
echo "Downloading MaxMind GeoLite2 databases..."
# Download GeoLite2-Country
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
-o GeoLite2-Country.tar.gz
# Download GeoLite2-ASN
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
-o GeoLite2-ASN.tar.gz
# Extract the .mmdb files
tar -xzf GeoLite2-Country.tar.gz --strip-components=1 --wildcards '*.mmdb'
tar -xzf GeoLite2-ASN.tar.gz --strip-components=1 --wildcards '*.mmdb'
# Verify files exist
if [ ! -f "GeoLite2-Country.mmdb" ]; then
echo "ERROR: Failed to download GeoLite2-Country.mmdb"
exit 1
fi
if [ ! -f "GeoLite2-ASN.mmdb" ]; then
echo "ERROR: Failed to download GeoLite2-ASN.mmdb"
exit 1
fi
# Clean up tar files
rm -f GeoLite2-Country.tar.gz GeoLite2-ASN.tar.gz
echo "MaxMind databases downloaded successfully"
ls -lh GeoLite2-*.mmdb
- name: Monitor storage space - name: Monitor storage space
run: | run: |
THRESHOLD=75 THRESHOLD=75

View File

@@ -49,14 +49,6 @@ COPY server/db/ios_models.json ./dist/ios_models.json
COPY server/db/mac_models.json ./dist/mac_models.json COPY server/db/mac_models.json ./dist/mac_models.json
COPY public ./public COPY public ./public
# Copy MaxMind databases for SaaS builds
ARG BUILD=oss
RUN mkdir -p ./maxmind
# This is only for saas
COPY --from=builder-dev /app/GeoLite2-Country.mmdb ./maxmind/GeoLite2-Country.mmdb
COPY --from=builder-dev /app/GeoLite2-ASN.mmdb ./maxmind/GeoLite2-ASN.mmdb
# OCI Image Labels - Build Args for dynamic values # OCI Image Labels - Build Args for dynamic values
ARG VERSION="dev" ARG VERSION="dev"
ARG REVISION="" ARG REVISION=""

View File

@@ -46,8 +46,6 @@ export class UsageService {
return null; return null;
} }
let orgIdToUse = await this.getBillingOrg(orgId, transaction);
// Truncate value to 11 decimal places // Truncate value to 11 decimal places
value = this.truncateValue(value); value = this.truncateValue(value);
@@ -59,6 +57,7 @@ export class UsageService {
try { try {
let usage; let usage;
if (transaction) { if (transaction) {
const orgIdToUse = await this.getBillingOrg(orgId, transaction);
usage = await this.internalAddUsage( usage = await this.internalAddUsage(
orgIdToUse, orgIdToUse,
featureId, featureId,
@@ -67,6 +66,7 @@ export class UsageService {
); );
} else { } else {
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
const orgIdToUse = await this.getBillingOrg(orgId, trx);
usage = await this.internalAddUsage( usage = await this.internalAddUsage(
orgIdToUse, orgIdToUse,
featureId, featureId,
@@ -92,7 +92,7 @@ export class UsageService {
const delay = baseDelay + jitter; const delay = baseDelay + jitter;
logger.warn( logger.warn(
`Deadlock detected for ${orgIdToUse}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms` `Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
); );
await new Promise((resolve) => setTimeout(resolve, delay)); await new Promise((resolve) => setTimeout(resolve, delay));
@@ -100,7 +100,7 @@ export class UsageService {
} }
logger.error( logger.error(
`Failed to add usage for ${orgIdToUse}/${featureId} after ${attempt} attempts:`, `Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
error error
); );
break; break;
@@ -169,7 +169,7 @@ export class UsageService {
return; return;
} }
let orgIdToUse = await this.getBillingOrg(orgId); const orgIdToUse = await this.getBillingOrg(orgId);
try { try {
// Truncate value to 11 decimal places if provided // Truncate value to 11 decimal places if provided
@@ -227,7 +227,7 @@ export class UsageService {
orgId: string, orgId: string,
featureId: FeatureId featureId: FeatureId
): Promise<string | null> { ): Promise<string | null> {
let orgIdToUse = await this.getBillingOrg(orgId); const orgIdToUse = await this.getBillingOrg(orgId);
const cacheKey = `customer_${orgIdToUse}_${featureId}`; const cacheKey = `customer_${orgIdToUse}_${featureId}`;
const cached = cache.get<string>(cacheKey); const cached = cache.get<string>(cacheKey);
@@ -274,7 +274,7 @@ export class UsageService {
return null; return null;
} }
let orgIdToUse = await this.getBillingOrg(orgId, trx); const orgIdToUse = await this.getBillingOrg(orgId, trx);
const usageId = `${orgIdToUse}-${featureId}`; const usageId = `${orgIdToUse}-${featureId}`;
@@ -382,7 +382,7 @@ export class UsageService {
return false; return false;
} }
let orgIdToUse = await this.getBillingOrg(orgId, trx); const orgIdToUse = await this.getBillingOrg(orgId, trx);
// This method should check the current usage against the limits set for the organization // This method should check the current usage against the limits set for the organization
// and kick out all of the sites on the org // and kick out all of the sites on the org

View File

@@ -23,14 +23,9 @@ export async function verifyApiKeyRoleAccess(
); );
} }
let allRoleIds: number[] = []; const { roleIds } = req.body;
if (!isNaN(singleRoleId)) { const allRoleIds =
// If roleId is provided in URL params, query params, or body (single), use it exclusively roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
allRoleIds = [singleRoleId];
} else if (req.body?.roleIds) {
// Only use body.roleIds if no single roleId was provided
allRoleIds = req.body.roleIds;
}
if (allRoleIds.length === 0) { if (allRoleIds.length === 0) {
return next(); return next();

View File

@@ -23,14 +23,8 @@ export async function verifyRoleAccess(
); );
} }
let allRoleIds: number[] = []; const roleIds = req.body?.roleIds;
if (!isNaN(singleRoleId)) { const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
// If roleId is provided in URL params, query params, or body (single), use it exclusively
allRoleIds = [singleRoleId];
} else if (req.body?.roleIds) {
// Only use body.roleIds if no single roleId was provided
allRoleIds = req.body.roleIds;
}
if (allRoleIds.length === 0) { if (allRoleIds.length === 0) {
return next(); return next();

View File

@@ -78,7 +78,8 @@ export async function getOrgTierData(
if ( if (
subscription.type === "tier1" || subscription.type === "tier1" ||
subscription.type === "tier2" || subscription.type === "tier2" ||
subscription.type === "tier3" subscription.type === "tier3" ||
subscription.type === "enterprise"
) { ) {
tier = subscription.type; tier = subscription.type;
active = true; active = true;

View File

@@ -72,15 +72,15 @@ export const privateConfigSchema = z.object({
db: z.int().nonnegative().optional().default(0) db: z.int().nonnegative().optional().default(0)
}) })
) )
.optional(),
tls: z
.object({
rejectUnauthorized: z
.boolean()
.optional()
.default(true)
})
.optional() .optional()
// tls: z
// .object({
// reject_unauthorized: z
// .boolean()
// .optional()
// .default(true)
// })
// .optional()
}) })
.optional(), .optional(),
gerbil: z gerbil: z

View File

@@ -108,15 +108,11 @@ class RedisManager {
port: redisConfig.port!, port: redisConfig.port!,
password: redisConfig.password, password: redisConfig.password,
db: redisConfig.db db: redisConfig.db
// tls: {
// rejectUnauthorized:
// redisConfig.tls?.reject_unauthorized || false
// }
}; };
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
if (redisConfig.tls) {
opts.tls = {
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
};
}
return opts; return opts;
} }
@@ -134,15 +130,11 @@ class RedisManager {
port: replica.port!, port: replica.port!,
password: replica.password, password: replica.password,
db: replica.db || redisConfig.db db: replica.db || redisConfig.db
// tls: {
// rejectUnauthorized:
// replica.tls?.reject_unauthorized || false
// }
}; };
// Enable TLS if configured (required for AWS ElastiCache in-transit encryption)
if (redisConfig.tls) {
opts.tls = {
rejectUnauthorized: redisConfig.tls.rejectUnauthorized ?? true
};
}
return opts; return opts;
} }

View File

@@ -139,7 +139,7 @@ export async function signSshKey(
if (!userOrg.pamUsername) { if (!userOrg.pamUsername) {
if (req.user?.email) { if (req.user?.email) {
// Extract username from email (first part before @) // Extract username from email (first part before @)
usernameToUse = req.user?.email.split("@")[0].replace(/[^a-zA-Z0-9_-]/g, ""); usernameToUse = req.user?.email.split("@")[0];
if (!usernameToUse) { if (!usernameToUse) {
return next( return next(
createHttpError( createHttpError(

View File

@@ -6,7 +6,7 @@ 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";
import logger from "@server/logger"; import logger from "@server/logger";
import { eq, and, ne } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
@@ -93,8 +93,7 @@ export async function updateClient(
.where( .where(
and( and(
eq(clients.niceId, niceId), eq(clients.niceId, niceId),
eq(clients.orgId, clients.orgId), eq(clients.orgId, clients.orgId)
ne(clients.clientId, clientId)
) )
) )
.limit(1); .limit(1);

View File

@@ -197,7 +197,6 @@ export async function updateSiteBandwidth(
usageService usageService
.checkLimitSet( .checkLimitSet(
orgId, orgId,
FeatureId.EGRESS_DATA_MB, FeatureId.EGRESS_DATA_MB,
bandwidthUsage bandwidthUsage
) )

View File

@@ -9,7 +9,7 @@ import {
Resource, Resource,
resources resources
} from "@server/db"; } from "@server/db";
import { eq, and, ne } from "drizzle-orm"; import { eq, and } 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";
@@ -33,15 +33,7 @@ const updateResourceParamsSchema = z.strictObject({
const updateHttpResourceBodySchema = z const updateHttpResourceBodySchema = z
.strictObject({ .strictObject({
name: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
niceId: z niceId: z.string().min(1).max(255).regex(/^[a-zA-Z0-9-]+$/, "niceId can only contain letters, numbers, and dashes").optional(),
.string()
.min(1)
.max(255)
.regex(
/^[a-zA-Z0-9-]+$/,
"niceId can only contain letters, numbers, and dashes"
)
.optional(),
subdomain: subdomainSchema.nullable().optional(), subdomain: subdomainSchema.nullable().optional(),
ssl: z.boolean().optional(), ssl: z.boolean().optional(),
sso: z.boolean().optional(), sso: z.boolean().optional(),
@@ -256,13 +248,14 @@ async function updateHttpResource(
.where( .where(
and( and(
eq(resources.niceId, updateData.niceId), eq(resources.niceId, updateData.niceId),
eq(resources.orgId, resource.orgId), eq(resources.orgId, resource.orgId)
ne(resources.resourceId, resource.resourceId) // exclude the current resource from the search
) )
) );
.limit(1);
if (existingResource) { if (
existingResource &&
existingResource.resourceId !== resource.resourceId
) {
return next( return next(
createHttpError( createHttpError(
HttpCode.CONFLICT, HttpCode.CONFLICT,
@@ -350,10 +343,7 @@ async function updateHttpResource(
headers = null; headers = null;
} }
const isLicensed = await isLicensedOrSubscribed( const isLicensed = await isLicensedOrSubscribed(resource.orgId, tierMatrix.maintencePage);
resource.orgId,
tierMatrix.maintencePage
);
if (!isLicensed) { if (!isLicensed) {
updateData.maintenanceModeEnabled = undefined; updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined; updateData.maintenanceModeType = undefined;

View File

@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { sites } from "@server/db"; import { sites } from "@server/db";
import { eq, and, ne } from "drizzle-orm"; import { eq, and } 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";
@@ -19,8 +19,8 @@ const updateSiteBodySchema = z
.strictObject({ .strictObject({
name: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
niceId: z.string().min(1).max(255).optional(), niceId: z.string().min(1).max(255).optional(),
dockerSocketEnabled: z.boolean().optional() dockerSocketEnabled: z.boolean().optional(),
// remoteSubnets: z.string().optional() remoteSubnets: z.string().optional()
// subdomain: z // subdomain: z
// .string() // .string()
// .min(1) // .min(1)
@@ -86,19 +86,18 @@ export async function updateSite(
// if niceId is provided, check if it's already in use by another site // if niceId is provided, check if it's already in use by another site
if (updateData.niceId) { if (updateData.niceId) {
const [existingSite] = await db const existingSite = await db
.select() .select()
.from(sites) .from(sites)
.where( .where(
and( and(
eq(sites.niceId, updateData.niceId), eq(sites.niceId, updateData.niceId),
eq(sites.orgId, sites.orgId), eq(sites.orgId, sites.orgId)
ne(sites.siteId, siteId)
) )
) )
.limit(1); .limit(1);
if (existingSite) { if (existingSite.length > 0 && existingSite[0].siteId !== siteId) {
return next( return next(
createHttpError( createHttpError(
HttpCode.CONFLICT, HttpCode.CONFLICT,
@@ -108,22 +107,22 @@ export async function updateSite(
} }
} }
// // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
// if (updateData.remoteSubnets) { if (updateData.remoteSubnets) {
// const subnets = updateData.remoteSubnets const subnets = updateData.remoteSubnets
// .split(",") .split(",")
// .map((s) => s.trim()); .map((s) => s.trim());
// for (const subnet of subnets) { for (const subnet of subnets) {
// if (!isValidCIDR(subnet)) { if (!isValidCIDR(subnet)) {
// return next( return next(
// createHttpError( createHttpError(
// HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
// `Invalid CIDR format: ${subnet}` `Invalid CIDR format: ${subnet}`
// ) )
// ); );
// } }
// } }
// } }
const updatedSite = await db const updatedSite = await db
.update(sites) .update(sites)

View File

@@ -20,7 +20,7 @@ export const isOrgSubscribed = cache(async (orgId: string) => {
try { try {
const subRes = await getCachedSubscription(orgId); const subRes = await getCachedSubscription(orgId);
subscribed = subscribed =
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3") && (subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3" || subRes.data.data.tier == "enterprise") &&
subRes.data.data.active; subRes.data.data.active;
} catch {} } catch {}
} }

View File

@@ -42,7 +42,8 @@ export function SubscriptionStatusProvider({
if ( if (
subscription.type == "tier1" || subscription.type == "tier1" ||
subscription.type == "tier2" || subscription.type == "tier2" ||
subscription.type == "tier3" subscription.type == "tier3" ||
subscription.type == "enterprise"
) { ) {
return { return {
tier: subscription.type, tier: subscription.type,
@@ -61,7 +62,7 @@ export function SubscriptionStatusProvider({
const isSubscribed = () => { const isSubscribed = () => {
const { tier, active } = getTier(); const { tier, active } = getTier();
return ( return (
(tier == "tier1" || tier == "tier2" || tier == "tier3") && (tier == "tier1" || tier == "tier2" || tier == "tier3" || tier == "enterprise") &&
active active
); );
}; };