mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-05 10:16:41 +00:00
Make easier to run in dev - fix a couple of things
This commit is contained in:
@@ -4,7 +4,7 @@ import { build } from "@server/build";
|
|||||||
|
|
||||||
const schema = [
|
const schema = [
|
||||||
path.join("server", "db", "pg", "schema.ts"),
|
path.join("server", "db", "pg", "schema.ts"),
|
||||||
path.join("server", "db", "pg", "pSchema.ts")
|
path.join("server", "db", "pg", "privateSchema.ts")
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import path from "path";
|
|||||||
|
|
||||||
const schema = [
|
const schema = [
|
||||||
path.join("server", "db", "sqlite", "schema.ts"),
|
path.join("server", "db", "sqlite", "schema.ts"),
|
||||||
path.join("server", "db", "sqlite", "pSchema.ts")
|
path.join("server", "db", "sqlite", "privateSchema.ts")
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
@@ -1542,8 +1542,8 @@
|
|||||||
"autoLoginError": "Auto Login Error",
|
"autoLoginError": "Auto Login Error",
|
||||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Managed Nodes",
|
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeDescription": "Self-host one or more nodes for tunnel exit servers",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes for tunnel exit servers",
|
||||||
"remoteExitNodes": "Nodes",
|
"remoteExitNodes": "Nodes",
|
||||||
"searchRemoteExitNodes": "Search nodes...",
|
"searchRemoteExitNodes": "Search nodes...",
|
||||||
"remoteExitNodeAdd": "Add Node",
|
"remoteExitNodeAdd": "Add Node",
|
||||||
@@ -1553,7 +1553,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
|
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
|
||||||
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
||||||
"remoteExitNodeDelete": "Delete Node",
|
"remoteExitNodeDelete": "Delete Node",
|
||||||
"sidebarRemoteExitNodes": "Nodes",
|
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Create Node",
|
"title": "Create Node",
|
||||||
"description": "Create a new node to extend your network connectivity",
|
"description": "Create a new node to extend your network connectivity",
|
||||||
|
|||||||
@@ -44,27 +44,25 @@ export function createApiServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const corsConfig = config.getRawConfig().server.cors;
|
const corsConfig = config.getRawConfig().server.cors;
|
||||||
|
const options = {
|
||||||
|
...(corsConfig?.origins
|
||||||
|
? { origin: corsConfig.origins }
|
||||||
|
: {
|
||||||
|
origin: (origin: any, callback: any) => {
|
||||||
|
callback(null, true);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...(corsConfig?.methods && { methods: corsConfig.methods }),
|
||||||
|
...(corsConfig?.allowed_headers && {
|
||||||
|
allowedHeaders: corsConfig.allowed_headers
|
||||||
|
}),
|
||||||
|
credentials: !(corsConfig?.credentials === false)
|
||||||
|
};
|
||||||
|
|
||||||
if (build == "oss") {
|
if (build == "oss" || !corsConfig) {
|
||||||
const options = {
|
|
||||||
...(corsConfig?.origins
|
|
||||||
? { origin: corsConfig.origins }
|
|
||||||
: {
|
|
||||||
origin: (origin: any, callback: any) => {
|
|
||||||
callback(null, true);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
...(corsConfig?.methods && { methods: corsConfig.methods }),
|
|
||||||
...(corsConfig?.allowed_headers && {
|
|
||||||
allowedHeaders: corsConfig.allowed_headers
|
|
||||||
}),
|
|
||||||
credentials: !(corsConfig?.credentials === false)
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug("Using CORS options", options);
|
logger.debug("Using CORS options", options);
|
||||||
|
|
||||||
apiServer.use(cors(options));
|
apiServer.use(cors(options));
|
||||||
} else {
|
} else if (corsConfig) {
|
||||||
// Use the custom CORS middleware with loginPage support
|
// Use the custom CORS middleware with loginPage support
|
||||||
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ interface StripeEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function noop() {
|
||||||
|
if (
|
||||||
|
build !== "saas" ||
|
||||||
|
!process.env.S3_BUCKET ||
|
||||||
|
!process.env.LOCAL_FILE_PATH
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export class UsageService {
|
export class UsageService {
|
||||||
private cache: NodeCache;
|
private cache: NodeCache;
|
||||||
private bucketName: string | undefined;
|
private bucketName: string | undefined;
|
||||||
@@ -41,7 +52,7 @@ export class UsageService {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
|
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
|
// this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
|
||||||
@@ -71,7 +82,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async initializeEventsDirectory(): Promise<void> {
|
private async initializeEventsDirectory(): Promise<void> {
|
||||||
if (!this.eventsDir) {
|
if (!this.eventsDir) {
|
||||||
logger.warn("Stripe local file path is not configured, skipping events directory initialization.");
|
logger.warn(
|
||||||
|
"Stripe local file path is not configured, skipping events directory initialization."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +96,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async uploadPendingEventFilesOnStartup(): Promise<void> {
|
private async uploadPendingEventFilesOnStartup(): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping leftover event file upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping leftover event file upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -112,7 +127,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Startup file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Startup file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -124,7 +141,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Empty startup file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Empty startup file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -135,8 +154,8 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logger.error("Failed to scan for leftover event files:", err);
|
logger.error("Failed to scan for leftover event files");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +165,7 @@ export class UsageService {
|
|||||||
value: number,
|
value: number,
|
||||||
transaction: any = null
|
transaction: any = null
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +198,12 @@ export class UsageService {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
usage = await this.internalAddUsage(orgId, featureId, value, trx);
|
usage = await this.internalAddUsage(
|
||||||
|
orgId,
|
||||||
|
featureId,
|
||||||
|
value,
|
||||||
|
trx
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,9 +213,10 @@ export class UsageService {
|
|||||||
return usage || null;
|
return usage || null;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Check if this is a deadlock error
|
// Check if this is a deadlock error
|
||||||
const isDeadlock = error?.code === '40P01' ||
|
const isDeadlock =
|
||||||
error?.cause?.code === '40P01' ||
|
error?.code === "40P01" ||
|
||||||
(error?.message && error.message.includes('deadlock'));
|
error?.cause?.code === "40P01" ||
|
||||||
|
(error?.message && error.message.includes("deadlock"));
|
||||||
|
|
||||||
if (isDeadlock && attempt < maxRetries) {
|
if (isDeadlock && attempt < maxRetries) {
|
||||||
attempt++;
|
attempt++;
|
||||||
@@ -204,7 +229,7 @@ export class UsageService {
|
|||||||
`Deadlock detected for ${orgId}/${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));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +272,8 @@ export class UsageService {
|
|||||||
set: {
|
set: {
|
||||||
latestValue: sql`${usage.latestValue} + ${value}`
|
latestValue: sql`${usage.latestValue} + ${value}`
|
||||||
}
|
}
|
||||||
}).returning();
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
return returnUsage;
|
return returnUsage;
|
||||||
}
|
}
|
||||||
@@ -268,7 +294,7 @@ export class UsageService {
|
|||||||
value?: number,
|
value?: number,
|
||||||
customerId?: string
|
customerId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -432,7 +458,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async writeEventToFile(event: StripeEvent): Promise<void> {
|
private async writeEventToFile(event: StripeEvent): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping event file write.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping event file write."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.currentEventFile) {
|
if (!this.currentEventFile) {
|
||||||
@@ -481,7 +509,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async uploadFileToS3(): Promise<void> {
|
private async uploadFileToS3(): Promise<void> {
|
||||||
if (!this.bucketName || !this.eventsDir) {
|
if (!this.bucketName || !this.eventsDir) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping S3 upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping S3 upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.currentEventFile) {
|
if (!this.currentEventFile) {
|
||||||
@@ -493,7 +523,9 @@ export class UsageService {
|
|||||||
|
|
||||||
// Check if this file is already being uploaded
|
// Check if this file is already being uploaded
|
||||||
if (this.uploadingFiles.has(fileName)) {
|
if (this.uploadingFiles.has(fileName)) {
|
||||||
logger.debug(`File ${fileName} is already being uploaded, skipping`);
|
logger.debug(
|
||||||
|
`File ${fileName} is already being uploaded, skipping`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +537,9 @@ export class UsageService {
|
|||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(`File ${fileName} does not exist, may have been already processed`);
|
logger.debug(
|
||||||
|
`File ${fileName} does not exist, may have been already processed`
|
||||||
|
);
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
// Reset current file if it was this file
|
// Reset current file if it was this file
|
||||||
if (this.currentEventFile === fileName) {
|
if (this.currentEventFile === fileName) {
|
||||||
@@ -525,7 +559,9 @@ export class UsageService {
|
|||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
// File may have been already deleted
|
// File may have been already deleted
|
||||||
logger.debug(`File ${fileName} was already deleted during cleanup`);
|
logger.debug(
|
||||||
|
`File ${fileName} was already deleted during cleanup`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.currentEventFile = null;
|
this.currentEventFile = null;
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
@@ -548,7 +584,9 @@ export class UsageService {
|
|||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
// File may have been already deleted by another process
|
// File may have been already deleted by another process
|
||||||
logger.debug(`File ${fileName} was already deleted during upload`);
|
logger.debug(
|
||||||
|
`File ${fileName} was already deleted during upload`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -559,10 +597,7 @@ export class UsageService {
|
|||||||
this.currentEventFile = null;
|
this.currentEventFile = null;
|
||||||
this.currentFileStartTime = 0;
|
this.currentFileStartTime = 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(`Failed to upload ${fileName} to S3:`, error);
|
||||||
`Failed to upload ${fileName} to S3:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
// Always remove from uploading set
|
// Always remove from uploading set
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
@@ -579,7 +614,7 @@ export class UsageService {
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
featureId: FeatureId
|
featureId: FeatureId
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,7 +688,7 @@ export class UsageService {
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
featureId: FeatureId
|
featureId: FeatureId
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
||||||
@@ -673,7 +708,9 @@ export class UsageService {
|
|||||||
*/
|
*/
|
||||||
private async uploadOldEventFiles(): Promise<void> {
|
private async uploadOldEventFiles(): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping old event file upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping old event file upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -684,7 +721,9 @@ export class UsageService {
|
|||||||
|
|
||||||
// Skip files that are already being uploaded
|
// Skip files that are already being uploaded
|
||||||
if (this.uploadingFiles.has(file)) {
|
if (this.uploadingFiles.has(file)) {
|
||||||
logger.debug(`Skipping file ${file} as it's already being uploaded`);
|
logger.debug(
|
||||||
|
`Skipping file ${file} as it's already being uploaded`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +765,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`File ${file} was already deleted during interval upload`);
|
logger.debug(
|
||||||
|
`File ${file} was already deleted during interval upload`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -743,7 +784,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Empty file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Empty file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -765,8 +808,13 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise<boolean> {
|
public async checkLimitSet(
|
||||||
if (build !== "saas") {
|
orgId: string,
|
||||||
|
kickSites = false,
|
||||||
|
featureId?: FeatureId,
|
||||||
|
usage?: Usage
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (noop()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 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
|
||||||
@@ -805,16 +853,30 @@ export class UsageService {
|
|||||||
if (usage) {
|
if (usage) {
|
||||||
currentUsage = usage;
|
currentUsage = usage;
|
||||||
} else {
|
} else {
|
||||||
currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId);
|
currentUsage = await this.getUsage(
|
||||||
|
orgId,
|
||||||
|
limit.featureId as FeatureId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const usageValue = currentUsage?.instantaneousValue || currentUsage?.latestValue || 0;
|
const usageValue =
|
||||||
logger.debug(`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`);
|
currentUsage?.instantaneousValue ||
|
||||||
logger.debug(`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`);
|
currentUsage?.latestValue ||
|
||||||
if (currentUsage && limit.value !== null && usageValue > limit.value) {
|
0;
|
||||||
|
logger.debug(
|
||||||
|
`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
currentUsage &&
|
||||||
|
limit.value !== null &&
|
||||||
|
usageValue > limit.value
|
||||||
|
) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
|
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
|
||||||
`${usageValue} > ${limit.value}`
|
`${usageValue} > ${limit.value}`
|
||||||
);
|
);
|
||||||
hasExceededLimits = true;
|
hasExceededLimits = true;
|
||||||
break; // Exit early if any limit is exceeded
|
break; // Exit early if any limit is exceeded
|
||||||
@@ -823,7 +885,9 @@ export class UsageService {
|
|||||||
|
|
||||||
// If any limits are exceeded, disconnect all sites for this organization
|
// If any limits are exceeded, disconnect all sites for this organization
|
||||||
if (hasExceededLimits && kickSites) {
|
if (hasExceededLimits && kickSites) {
|
||||||
logger.warn(`Disconnecting all sites for org ${orgId} due to exceeded limits`);
|
logger.warn(
|
||||||
|
`Disconnecting all sites for org ${orgId} due to exceeded limits`
|
||||||
|
);
|
||||||
|
|
||||||
// Get all sites for this organization
|
// Get all sites for this organization
|
||||||
const orgSites = await db
|
const orgSites = await db
|
||||||
@@ -832,7 +896,7 @@ export class UsageService {
|
|||||||
.where(eq(sites.orgId, orgId));
|
.where(eq(sites.orgId, orgId));
|
||||||
|
|
||||||
// Mark all sites as offline and send termination messages
|
// Mark all sites as offline and send termination messages
|
||||||
const siteUpdates = orgSites.map(site => site.siteId);
|
const siteUpdates = orgSites.map((site) => site.siteId);
|
||||||
|
|
||||||
if (siteUpdates.length > 0) {
|
if (siteUpdates.length > 0) {
|
||||||
// Send termination messages to newt sites
|
// Send termination messages to newt sites
|
||||||
@@ -853,17 +917,21 @@ export class UsageService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Don't await to prevent blocking
|
// Don't await to prevent blocking
|
||||||
sendToClient(newt.newtId, payload).catch((error: any) => {
|
sendToClient(newt.newtId, payload).catch(
|
||||||
logger.error(
|
(error: any) => {
|
||||||
`Failed to send termination message to newt ${newt.newtId}:`,
|
logger.error(
|
||||||
error
|
`Failed to send termination message to newt ${newt.newtId}:`,
|
||||||
);
|
error
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`);
|
logger.info(
|
||||||
|
`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import privateConfig from "#private/lib/config";
|
import privateConfig from "#private/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { build } from "@server/build";
|
import { noop } from "@server/lib/billing/usageService";
|
||||||
|
|
||||||
let stripe: Stripe | undefined = undefined;
|
let stripe: Stripe | undefined = undefined;
|
||||||
if (build == "saas") {
|
if (!noop()) {
|
||||||
const stripeApiKey = privateConfig.getRawPrivateConfig().stripe?.secret_key;
|
const stripeApiKey = privateConfig.getRawPrivateConfig().stripe?.secret_key;
|
||||||
if (!stripeApiKey) {
|
if (!stripeApiKey) {
|
||||||
logger.error("Stripe secret key is not configured");
|
logger.error("Stripe secret key is not configured");
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
import { unauthenticated as ua, authenticated as a } from "@server/routers/external";
|
import { unauthenticated as ua, authenticated as a, authRouter as aa } from "@server/routers/external";
|
||||||
|
|
||||||
export const authenticated = a;
|
export const authenticated = a;
|
||||||
export const unauthenticated = ua;
|
export const unauthenticated = ua;
|
||||||
|
export const authRouter = aa;
|
||||||
|
|
||||||
unauthenticated.post(
|
unauthenticated.post(
|
||||||
"/quick-start",
|
"/quick-start",
|
||||||
@@ -227,8 +228,6 @@ authenticated.get(
|
|||||||
loginPage.getLoginPage
|
loginPage.getLoginPage
|
||||||
);
|
);
|
||||||
|
|
||||||
export const authRouter = Router();
|
|
||||||
|
|
||||||
authRouter.post(
|
authRouter.post(
|
||||||
"/remoteExitNode/get-token",
|
"/remoteExitNode/get-token",
|
||||||
rateLimit({
|
rateLimit({
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import { exchangeSession } from "@server/routers/badger";
|
|||||||
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
|
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
|
||||||
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
||||||
import { maxmindLookup } from "@server/db/maxmind";
|
import { maxmindLookup } from "@server/db/maxmind";
|
||||||
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
|
|
||||||
// Zod schemas for request validation
|
// Zod schemas for request validation
|
||||||
const getResourceByDomainParamsSchema = z
|
const getResourceByDomainParamsSchema = z
|
||||||
@@ -162,6 +163,14 @@ const validateResourceSessionTokenBodySchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const validateResourceAccessTokenBodySchema = z
|
||||||
|
.object({
|
||||||
|
accessTokenId: z.string().optional(),
|
||||||
|
resourceId: z.number().optional(),
|
||||||
|
accessToken: z.string()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
// Certificates by domains query validation
|
// Certificates by domains query validation
|
||||||
const getCertificatesByDomainsQuerySchema = z
|
const getCertificatesByDomainsQuerySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -215,6 +224,33 @@ export type UserSessionWithUser = {
|
|||||||
export const hybridRouter = Router();
|
export const hybridRouter = Router();
|
||||||
hybridRouter.use(verifySessionRemoteExitNodeMiddleware);
|
hybridRouter.use(verifySessionRemoteExitNodeMiddleware);
|
||||||
|
|
||||||
|
hybridRouter.get(
|
||||||
|
"/general-config",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
return response(res, {
|
||||||
|
data: {
|
||||||
|
resource_session_request_param:
|
||||||
|
config.getRawConfig().server.resource_session_request_param,
|
||||||
|
resource_access_token_headers:
|
||||||
|
config.getRawConfig().server.resource_access_token_headers,
|
||||||
|
resource_access_token_param:
|
||||||
|
config.getRawConfig().server.resource_access_token_param,
|
||||||
|
session_cookie_name:
|
||||||
|
config.getRawConfig().server.session_cookie_name,
|
||||||
|
require_email_verification:
|
||||||
|
config.getRawConfig().flags?.require_email_verification ||
|
||||||
|
false,
|
||||||
|
resource_session_length_hours:
|
||||||
|
config.getRawConfig().server.resource_session_length_hours
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "General config retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
hybridRouter.get(
|
hybridRouter.get(
|
||||||
"/traefik-config",
|
"/traefik-config",
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -1101,6 +1137,52 @@ hybridRouter.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Validate resource session token
|
||||||
|
hybridRouter.post(
|
||||||
|
"/resource/:resourceId/access-token/verify",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedBody = validateResourceAccessTokenBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken, resourceId, accessTokenId } = parsedBody.data;
|
||||||
|
|
||||||
|
const result = await verifyResourceAccessToken({
|
||||||
|
accessTokenId,
|
||||||
|
accessToken,
|
||||||
|
resourceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: result,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: result.valid
|
||||||
|
? "Resource access token is valid"
|
||||||
|
: "Resource access token is invalid or expired",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to validate resource session token"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const geoIpLookupParamsSchema = z.object({
|
const geoIpLookupParamsSchema = z.object({
|
||||||
ip: z.string().ip()
|
ip: z.string().ip()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -84,30 +84,25 @@ export async function createRemoteExitNode(
|
|||||||
orgId,
|
orgId,
|
||||||
FeatureId.REMOTE_EXIT_NODES
|
FeatureId.REMOTE_EXIT_NODES
|
||||||
);
|
);
|
||||||
if (!usage) {
|
if (usage) {
|
||||||
return next(
|
const rejectRemoteExitNodes = await usageService.checkLimitSet(
|
||||||
createHttpError(
|
orgId,
|
||||||
HttpCode.NOT_FOUND,
|
false,
|
||||||
"No usage data found for this organization"
|
FeatureId.REMOTE_EXIT_NODES,
|
||||||
)
|
{
|
||||||
);
|
...usage,
|
||||||
}
|
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||||
const rejectRemoteExitNodes = await usageService.checkLimitSet(
|
} // We need to add one to know if we are violating the limit
|
||||||
orgId,
|
|
||||||
false,
|
|
||||||
FeatureId.REMOTE_EXIT_NODES,
|
|
||||||
{
|
|
||||||
...usage,
|
|
||||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
|
||||||
} // We need to add one to know if we are violating the limit
|
|
||||||
);
|
|
||||||
if (rejectRemoteExitNodes) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (rejectRemoteExitNodes) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretHash = await hashPassword(secret);
|
const secretHash = await hashPassword(secret);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"#private/*": ["../server/private/*"],
|
"#private/*": ["../server/private/*"],
|
||||||
"#open/*": ["../server/*"],
|
"#open/*": ["../server/*"],
|
||||||
"#closed/*": ["../server/private/*"],
|
"#closed/*": ["../server/private/*"],
|
||||||
"#dynamic/*": ["../server/*"]
|
"#dynamic/*": ["../server/private/*"]
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user