mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-22 04:29:05 +02:00
Merge branch 'dev' into feat/redirect-tables
This commit is contained in:
@@ -13,3 +13,4 @@
|
||||
|
||||
export * from "./getOrgTierData";
|
||||
export * from "./createCustomer";
|
||||
export * from "./linkEmailOrg";
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import logger from "@server/logger";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export async function linkEmailOrg(
|
||||
orgId: string,
|
||||
email: string | null | undefined
|
||||
): Promise<void> {
|
||||
if (build !== "saas") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/link-email-org`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"api-key":
|
||||
privateConfig.getRawPrivateConfig().server
|
||||
.fossorial_api_key!,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ email, orgId })
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
logger.error(
|
||||
`Fossorial API returned ${response.status} when linking email ${email} to orgId ${orgId}: ${await response.text()}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error notifying Fossorial API of email/org link for orgId ${orgId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,8 @@ export async function getTraefikConfig(
|
||||
tlsServerName: resources.tlsServerName,
|
||||
setHostHeader: resources.setHostHeader,
|
||||
enableProxy: resources.enableProxy,
|
||||
headers: resources.headers,
|
||||
requestHeaders: resources.requestHeaders,
|
||||
responseHeaders: resources.responseHeaders,
|
||||
proxyProtocol: resources.proxyProtocol,
|
||||
proxyProtocolVersion: resources.proxyProtocolVersion,
|
||||
wildcard: resources.wildcard,
|
||||
@@ -280,7 +281,8 @@ export async function getTraefikConfig(
|
||||
setHostHeader: row.setHostHeader,
|
||||
enableProxy: row.enableProxy,
|
||||
targets: [],
|
||||
headers: row.headers,
|
||||
requestHeaders: row.requestHeaders,
|
||||
responseHeaders: row.responseHeaders,
|
||||
proxyProtocol: row.proxyProtocol,
|
||||
proxyProtocolVersion: row.proxyProtocolVersion ?? 1,
|
||||
path: row.path, // the targets will all have the same path
|
||||
@@ -811,7 +813,8 @@ export async function getTraefikConfig(
|
||||
);
|
||||
|
||||
const customHeadersMiddleware = buildCustomHeadersMiddleware(
|
||||
resource.headers,
|
||||
resource.requestHeaders,
|
||||
resource.responseHeaders,
|
||||
resource.setHostHeader,
|
||||
resource.resourceId
|
||||
);
|
||||
|
||||
@@ -50,6 +50,27 @@ type ValidateLicenseAPIResponse = {
|
||||
status: number;
|
||||
};
|
||||
|
||||
// Ranks license tiers so that when multiple license keys are active, the
|
||||
// highest tier among them wins. Order: personal < tier1 < tier2 < ... <
|
||||
// tier[n] < enterprise. Tier numbers are parsed so this scales to any
|
||||
// tier[n] without needing updates here.
|
||||
function tierRank(tier?: LicenseKeyTier): number {
|
||||
if (!tier) {
|
||||
return -1;
|
||||
}
|
||||
if (tier === "enterprise") {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
if (tier === "personal") {
|
||||
return 0;
|
||||
}
|
||||
const match = /^tier(\d+)$/.exec(tier);
|
||||
if (match) {
|
||||
return parseInt(match[1], 10);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
type TokenPayload = {
|
||||
valid: boolean;
|
||||
type: LicenseKeyType;
|
||||
@@ -276,6 +297,11 @@ LQIDAQAB
|
||||
if (!apiResponse?.success) {
|
||||
throw new Error(apiResponse?.error);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`License server response: ${JSON.stringify(apiResponse)}`
|
||||
);
|
||||
|
||||
// Reset failure count on success
|
||||
this.phoneHomeFailureCount = 0;
|
||||
} catch (e) {
|
||||
@@ -338,6 +364,11 @@ LQIDAQAB
|
||||
licenseKeyRes,
|
||||
this.publicKey
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Decoded license key ${key.licenseKey}: ${JSON.stringify(payload)}`
|
||||
);
|
||||
|
||||
cached.valid = payload.valid;
|
||||
cached.type = payload.type;
|
||||
cached.tier = payload.tier;
|
||||
@@ -370,13 +401,52 @@ LQIDAQAB
|
||||
}
|
||||
}
|
||||
|
||||
// Personal-tier licenses cannot coexist with a paid tier: if any
|
||||
// valid host key is above personal, personal-tier keys are
|
||||
// invalidated so they don't contribute to the totals below.
|
||||
const hasHigherTierValidKey = keys.some((key) => {
|
||||
const cached = newCache.get(key.licenseKey)!;
|
||||
return (
|
||||
cached.type === "host" &&
|
||||
cached.valid &&
|
||||
tierRank(cached.tier) > tierRank("personal")
|
||||
);
|
||||
});
|
||||
|
||||
if (hasHigherTierValidKey) {
|
||||
for (const key of keys) {
|
||||
const cached = newCache.get(key.licenseKey)!;
|
||||
if (
|
||||
cached.type === "host" &&
|
||||
cached.valid &&
|
||||
cached.tier === "personal"
|
||||
) {
|
||||
logger.debug(
|
||||
`Invalidating personal license key ${key.licenseKey} because a higher tier license is present`
|
||||
);
|
||||
cached.valid = false;
|
||||
newCache.set(key.licenseKey, cached);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute host status: quantity = users, quantity_2 = sites
|
||||
// When multiple host keys are active, prefer a valid key over an
|
||||
// invalid one, and among equally-valid keys prefer the highest tier.
|
||||
let selectedHostKey: LicenseKeyCache | undefined;
|
||||
for (const key of keys) {
|
||||
const cached = newCache.get(key.licenseKey)!;
|
||||
|
||||
if (cached.type === "host") {
|
||||
status.isLicenseValid = cached.valid;
|
||||
status.tier = cached.tier;
|
||||
if (
|
||||
!selectedHostKey ||
|
||||
(cached.valid && !selectedHostKey.valid) ||
|
||||
(cached.valid === selectedHostKey.valid &&
|
||||
tierRank(cached.tier) >
|
||||
tierRank(selectedHostKey.tier))
|
||||
) {
|
||||
selectedHostKey = cached;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cached.valid) {
|
||||
@@ -393,6 +463,11 @@ LQIDAQAB
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedHostKey) {
|
||||
status.isLicenseValid = selectedHostKey.valid;
|
||||
status.tier = selectedHostKey.tier;
|
||||
}
|
||||
|
||||
// Invalidate license if over user or site limits
|
||||
if (
|
||||
(status.maxSites !== undefined &&
|
||||
@@ -415,6 +490,8 @@ LQIDAQAB
|
||||
this.checkInProgress = false;
|
||||
}
|
||||
|
||||
logger.debug(`Computed license status: ${JSON.stringify(status)}`);
|
||||
|
||||
this.statusCache.set(this.statusKey, status, 0);
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export async function exportConnectionAuditLogs(
|
||||
|
||||
const baseQuery = queryConnection(data);
|
||||
|
||||
const log = await baseQuery.limit(data.limit).offset(data.offset);
|
||||
const log = await baseQuery.limit(MAX_EXPORT_LIMIT);
|
||||
|
||||
const csvData = generateCSV(log);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user