mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Merge branch 'dev' into msg-delivery
This commit is contained in:
22
server/private/lib/blueprints/MaintenanceSchema.ts
Normal file
22
server/private/lib/blueprints/MaintenanceSchema.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 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 { z } from "zod";
|
||||
|
||||
export const MaintenanceSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
type: z.enum(["forced", "automatic"]).optional(),
|
||||
title: z.string().max(255).nullable().optional(),
|
||||
message: z.string().max(2000).nullable().optional(),
|
||||
"estimated-time": z.string().max(100).nullable().optional()
|
||||
});
|
||||
@@ -23,10 +23,10 @@ import {
|
||||
} from "@server/lib/checkOrgAccessPolicy";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
export async function enforceResourceSessionLength(
|
||||
export function enforceResourceSessionLength(
|
||||
resourceSession: ResourceSession,
|
||||
org: Org
|
||||
): Promise<{ valid: boolean; error?: string }> {
|
||||
): { valid: boolean; error?: string } {
|
||||
if (org.maxSessionLengthHours) {
|
||||
const sessionIssuedAt = resourceSession.issuedAt; // may be null
|
||||
const maxSessionLengthHours = org.maxSessionLengthHours;
|
||||
|
||||
@@ -161,8 +161,11 @@ async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
||||
|
||||
if (!cachedCountryCode) {
|
||||
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
||||
// Cache for longer since IP geolocation doesn't change frequently
|
||||
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||
// Only cache successful lookups to avoid filling cache with undefined values
|
||||
if (cachedCountryCode) {
|
||||
// Cache for longer since IP geolocation doesn't change frequently
|
||||
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
return cachedCountryCode;
|
||||
|
||||
@@ -47,24 +47,33 @@ const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||
const redirectToRootMiddlewareName = "redirect-to-root";
|
||||
const badgerMiddlewareName = "badger";
|
||||
|
||||
// Define extended target type with site information
|
||||
type TargetWithSite = Target & {
|
||||
resourceId: number;
|
||||
targetId: number;
|
||||
ip: string | null;
|
||||
method: string | null;
|
||||
port: number | null;
|
||||
internalPort: number | null;
|
||||
enabled: boolean;
|
||||
health: string | null;
|
||||
site: {
|
||||
siteId: number;
|
||||
type: string;
|
||||
subnet: string | null;
|
||||
exitNodeId: number | null;
|
||||
online: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getTraefikConfig(
|
||||
exitNodeId: number,
|
||||
siteTypes: string[],
|
||||
filterOutNamespaceDomains = false,
|
||||
generateLoginPageRouters = false,
|
||||
allowRawResources = true
|
||||
allowRawResources = true,
|
||||
allowMaintenancePage = true
|
||||
): Promise<any> {
|
||||
// Define extended target type with site information
|
||||
type TargetWithSite = Target & {
|
||||
site: {
|
||||
siteId: number;
|
||||
type: string;
|
||||
subnet: string | null;
|
||||
exitNodeId: number | null;
|
||||
online: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// Get resources with their targets and sites in a single optimized query
|
||||
// Start from sites on this exit node, then join to targets and resources
|
||||
const resourcesWithTargetsAndSites = await db
|
||||
@@ -87,6 +96,13 @@ export async function getTraefikConfig(
|
||||
headers: resources.headers,
|
||||
proxyProtocol: resources.proxyProtocol,
|
||||
proxyProtocolVersion: resources.proxyProtocolVersion,
|
||||
|
||||
maintenanceModeEnabled: resources.maintenanceModeEnabled,
|
||||
maintenanceModeType: resources.maintenanceModeType,
|
||||
maintenanceTitle: resources.maintenanceTitle,
|
||||
maintenanceMessage: resources.maintenanceMessage,
|
||||
maintenanceEstimatedTime: resources.maintenanceEstimatedTime,
|
||||
|
||||
// Target fields
|
||||
targetId: targets.targetId,
|
||||
targetEnabled: targets.enabled,
|
||||
@@ -140,10 +156,6 @@ export async function getTraefikConfig(
|
||||
sql`(${build != "saas" ? 1 : 0} = 1)` // Dont allow undefined local sites in cloud
|
||||
)
|
||||
),
|
||||
or(
|
||||
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
||||
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
||||
),
|
||||
inArray(sites.type, siteTypes),
|
||||
allowRawResources
|
||||
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
|
||||
@@ -220,7 +232,13 @@ export async function getTraefikConfig(
|
||||
rewritePathType: row.rewritePathType,
|
||||
priority: priority, // may be null, we fallback later
|
||||
domainCertResolver: row.domainCertResolver,
|
||||
preferWildcardCert: row.preferWildcardCert
|
||||
preferWildcardCert: row.preferWildcardCert,
|
||||
|
||||
maintenanceModeEnabled: row.maintenanceModeEnabled,
|
||||
maintenanceModeType: row.maintenanceModeType,
|
||||
maintenanceTitle: row.maintenanceTitle,
|
||||
maintenanceMessage: row.maintenanceMessage,
|
||||
maintenanceEstimatedTime: row.maintenanceEstimatedTime
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,6 +251,7 @@ export async function getTraefikConfig(
|
||||
port: row.port,
|
||||
internalPort: row.internalPort,
|
||||
enabled: row.targetEnabled,
|
||||
health: row.hcHealth,
|
||||
site: {
|
||||
siteId: row.siteId,
|
||||
type: row.siteType,
|
||||
@@ -278,7 +297,7 @@ export async function getTraefikConfig(
|
||||
|
||||
// get the key and the resource
|
||||
for (const [key, resource] of resourcesMap.entries()) {
|
||||
const targets = resource.targets;
|
||||
const targets = resource.targets as TargetWithSite[];
|
||||
|
||||
const routerName = `${key}-${resource.name}-router`;
|
||||
const serviceName = `${key}-${resource.name}-service`;
|
||||
@@ -308,20 +327,49 @@ export async function getTraefikConfig(
|
||||
config_output.http.services = {};
|
||||
}
|
||||
|
||||
const domainParts = fullDomain.split(".");
|
||||
let wildCard;
|
||||
if (domainParts.length <= 2) {
|
||||
wildCard = `*.${domainParts.join(".")}`;
|
||||
const additionalMiddlewares =
|
||||
config.getRawConfig().traefik.additional_middlewares || [];
|
||||
|
||||
const routerMiddlewares = [
|
||||
badgerMiddlewareName,
|
||||
...additionalMiddlewares
|
||||
];
|
||||
|
||||
let rule = `Host(\`${fullDomain}\`)`;
|
||||
|
||||
// priority logic
|
||||
let priority: number;
|
||||
if (resource.priority && resource.priority != 100) {
|
||||
priority = resource.priority;
|
||||
} else {
|
||||
wildCard = `*.${domainParts.slice(1).join(".")}`;
|
||||
priority = 100;
|
||||
if (resource.path && resource.pathMatchType) {
|
||||
priority += 10;
|
||||
if (resource.pathMatchType === "exact") {
|
||||
priority += 5;
|
||||
} else if (resource.pathMatchType === "prefix") {
|
||||
priority += 3;
|
||||
} else if (resource.pathMatchType === "regex") {
|
||||
priority += 2;
|
||||
}
|
||||
if (resource.path === "/") {
|
||||
priority = 1; // lowest for catch-all
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resource.subdomain) {
|
||||
wildCard = resource.fullDomain;
|
||||
if (resource.ssl) {
|
||||
config_output.http.routers![routerName + "-redirect"] = {
|
||||
entryPoints: [
|
||||
config.getRawConfig().traefik.http_entrypoint
|
||||
],
|
||||
middlewares: [redirectHttpsMiddlewareName],
|
||||
service: serviceName,
|
||||
rule: rule,
|
||||
priority: priority
|
||||
};
|
||||
}
|
||||
|
||||
const configDomain = config.getDomain(resource.domainId);
|
||||
|
||||
let tls = {};
|
||||
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
const domainParts = fullDomain.split(".");
|
||||
@@ -387,13 +435,105 @@ export async function getTraefikConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const additionalMiddlewares =
|
||||
config.getRawConfig().traefik.additional_middlewares || [];
|
||||
const availableServers = targets.filter((target) => {
|
||||
if (!target.enabled) return false;
|
||||
|
||||
const routerMiddlewares = [
|
||||
badgerMiddlewareName,
|
||||
...additionalMiddlewares
|
||||
];
|
||||
if (!target.site.online) return false;
|
||||
|
||||
if (target.health == "unhealthy") return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const hasHealthyServers = availableServers.length > 0;
|
||||
|
||||
let showMaintenancePage = false;
|
||||
if (resource.maintenanceModeEnabled) {
|
||||
if (resource.maintenanceModeType === "forced") {
|
||||
showMaintenancePage = true;
|
||||
// logger.debug(
|
||||
// `Resource ${resource.name} (${fullDomain}) is in FORCED maintenance mode`
|
||||
// );
|
||||
} else if (resource.maintenanceModeType === "automatic") {
|
||||
showMaintenancePage = !hasHealthyServers;
|
||||
if (showMaintenancePage) {
|
||||
logger.warn(
|
||||
`Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showMaintenancePage) {
|
||||
const maintenanceServiceName = `${key}-maintenance-service`;
|
||||
const maintenanceRouterName = `${key}-maintenance-router`;
|
||||
const rewriteMiddlewareName = `${key}-maintenance-rewrite`;
|
||||
|
||||
const entrypointHttp =
|
||||
config.getRawConfig().traefik.http_entrypoint;
|
||||
const entrypointHttps =
|
||||
config.getRawConfig().traefik.https_entrypoint;
|
||||
|
||||
const fullDomain = resource.fullDomain;
|
||||
const domainParts = fullDomain.split(".");
|
||||
const wildCard = resource.subdomain
|
||||
? `*.${domainParts.slice(1).join(".")}`
|
||||
: fullDomain;
|
||||
|
||||
const maintenancePort = config.getRawConfig().server.next_port;
|
||||
const maintenanceHost =
|
||||
config.getRawConfig().server.internal_hostname;
|
||||
|
||||
config_output.http.services[maintenanceServiceName] = {
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `http://${maintenanceHost}:${maintenancePort}`
|
||||
}
|
||||
],
|
||||
passHostHeader: true
|
||||
}
|
||||
};
|
||||
|
||||
// middleware to rewrite path to /maintenance-screen
|
||||
if (!config_output.http.middlewares) {
|
||||
config_output.http.middlewares = {};
|
||||
}
|
||||
|
||||
config_output.http.middlewares[rewriteMiddlewareName] = {
|
||||
replacePathRegex: {
|
||||
regex: "^/(.*)",
|
||||
replacement: "/maintenance-screen"
|
||||
}
|
||||
};
|
||||
|
||||
config_output.http.routers[maintenanceRouterName] = {
|
||||
entryPoints: [
|
||||
resource.ssl ? entrypointHttps : entrypointHttp
|
||||
],
|
||||
service: maintenanceServiceName,
|
||||
middlewares: [rewriteMiddlewareName],
|
||||
rule: rule,
|
||||
priority: 2000,
|
||||
...(resource.ssl ? { tls } : {})
|
||||
};
|
||||
|
||||
// Router to allow Next.js assets to load without rewrite
|
||||
config_output.http.routers[`${maintenanceRouterName}-assets`] =
|
||||
{
|
||||
entryPoints: [
|
||||
resource.ssl ? entrypointHttps : entrypointHttp
|
||||
],
|
||||
service: maintenanceServiceName,
|
||||
rule: `Host(\`${fullDomain}\`) && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`,
|
||||
priority: 2001,
|
||||
...(resource.ssl ? { tls } : {})
|
||||
};
|
||||
|
||||
// logger.info(`Maintenance mode active for ${fullDomain}`);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle path rewriting middleware
|
||||
if (
|
||||
@@ -485,29 +625,6 @@ export async function getTraefikConfig(
|
||||
}
|
||||
}
|
||||
|
||||
let rule = `Host(\`${fullDomain}\`)`;
|
||||
|
||||
// priority logic
|
||||
let priority: number;
|
||||
if (resource.priority && resource.priority != 100) {
|
||||
priority = resource.priority;
|
||||
} else {
|
||||
priority = 100;
|
||||
if (resource.path && resource.pathMatchType) {
|
||||
priority += 10;
|
||||
if (resource.pathMatchType === "exact") {
|
||||
priority += 5;
|
||||
} else if (resource.pathMatchType === "prefix") {
|
||||
priority += 3;
|
||||
} else if (resource.pathMatchType === "regex") {
|
||||
priority += 2;
|
||||
}
|
||||
if (resource.path === "/") {
|
||||
priority = 1; // lowest for catch-all
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.path && resource.pathMatchType) {
|
||||
//priority += 1;
|
||||
// add path to rule based on match type
|
||||
@@ -538,18 +655,6 @@ export async function getTraefikConfig(
|
||||
...(resource.ssl ? { tls } : {})
|
||||
};
|
||||
|
||||
if (resource.ssl) {
|
||||
config_output.http.routers![routerName + "-redirect"] = {
|
||||
entryPoints: [
|
||||
config.getRawConfig().traefik.http_entrypoint
|
||||
],
|
||||
middlewares: [redirectHttpsMiddlewareName],
|
||||
service: serviceName,
|
||||
rule: rule,
|
||||
priority: priority
|
||||
};
|
||||
}
|
||||
|
||||
config_output.http.services![serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: (() => {
|
||||
@@ -559,17 +664,21 @@ export async function getTraefikConfig(
|
||||
// RECEIVE BANDWIDTH ENDPOINT.
|
||||
|
||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||
const anySitesOnline = (
|
||||
targets as TargetWithSite[]
|
||||
).some((target: TargetWithSite) => target.site.online);
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
);
|
||||
|
||||
return (
|
||||
(targets as TargetWithSite[])
|
||||
.filter((target: TargetWithSite) => {
|
||||
targets
|
||||
.filter((target) => {
|
||||
if (!target.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.health == "unhealthy") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If any sites are online, exclude offline sites
|
||||
if (anySitesOnline && !target.site.online) {
|
||||
return false;
|
||||
@@ -597,7 +706,7 @@ export async function getTraefikConfig(
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((target: TargetWithSite) => {
|
||||
.map((target) => {
|
||||
if (
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
@@ -683,12 +792,12 @@ export async function getTraefikConfig(
|
||||
loadBalancer: {
|
||||
servers: (() => {
|
||||
// Check if any sites are online
|
||||
const anySitesOnline = (
|
||||
targets as TargetWithSite[]
|
||||
).some((target: TargetWithSite) => target.site.online);
|
||||
const anySitesOnline = targets.some(
|
||||
(target) => target.site.online
|
||||
);
|
||||
|
||||
return (targets as TargetWithSite[])
|
||||
.filter((target: TargetWithSite) => {
|
||||
return targets
|
||||
.filter((target) => {
|
||||
if (!target.enabled) {
|
||||
return false;
|
||||
}
|
||||
@@ -715,7 +824,7 @@ export async function getTraefikConfig(
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((target: TargetWithSite) => {
|
||||
.map((target) => {
|
||||
if (
|
||||
target.site.type === "local" ||
|
||||
target.site.type === "wireguard"
|
||||
|
||||
@@ -36,8 +36,11 @@ import {
|
||||
LoginPage,
|
||||
resourceHeaderAuth,
|
||||
ResourceHeaderAuth,
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
ResourceHeaderAuthExtendedCompatibility,
|
||||
orgs,
|
||||
requestAuditLog
|
||||
requestAuditLog,
|
||||
Org
|
||||
} from "@server/db";
|
||||
import {
|
||||
resources,
|
||||
@@ -76,6 +79,8 @@ import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
||||
import { maxmindLookup } from "@server/db/maxmind";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import semver from "semver";
|
||||
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
||||
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
||||
|
||||
// Zod schemas for request validation
|
||||
const getResourceByDomainParamsSchema = z.strictObject({
|
||||
@@ -91,6 +96,12 @@ const getUserOrgRoleParamsSchema = z.strictObject({
|
||||
orgId: z.string().min(1, "Organization ID is required")
|
||||
});
|
||||
|
||||
const getUserOrgSessionVerifySchema = z.strictObject({
|
||||
userId: z.string().min(1, "User ID is required"),
|
||||
orgId: z.string().min(1, "Organization ID is required"),
|
||||
sessionId: z.string().min(1, "Session ID is required")
|
||||
});
|
||||
|
||||
const getRoleResourceAccessParamsSchema = z.strictObject({
|
||||
roleId: z
|
||||
.string()
|
||||
@@ -174,6 +185,8 @@ export type ResourceWithAuth = {
|
||||
pincode: ResourcePincode | null;
|
||||
password: ResourcePassword | null;
|
||||
headerAuth: ResourceHeaderAuth | null;
|
||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||
org: Org
|
||||
};
|
||||
|
||||
export type UserSessionWithUser = {
|
||||
@@ -497,6 +510,14 @@ hybridRouter.get(
|
||||
resourceHeaderAuth,
|
||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||
)
|
||||
.leftJoin(
|
||||
resourceHeaderAuthExtendedCompatibility,
|
||||
eq(
|
||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||
resources.resourceId
|
||||
)
|
||||
)
|
||||
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||
.where(eq(resources.fullDomain, domain))
|
||||
.limit(1);
|
||||
|
||||
@@ -529,7 +550,10 @@ hybridRouter.get(
|
||||
resource: result.resources,
|
||||
pincode: result.resourcePincode,
|
||||
password: result.resourcePassword,
|
||||
headerAuth: result.resourceHeaderAuth
|
||||
headerAuth: result.resourceHeaderAuth,
|
||||
headerAuthExtendedCompatibility:
|
||||
result.resourceHeaderAuthExtendedCompatibility,
|
||||
org: result.orgs
|
||||
};
|
||||
|
||||
return response<ResourceWithAuth>(res, {
|
||||
@@ -809,6 +833,69 @@ hybridRouter.get(
|
||||
}
|
||||
);
|
||||
|
||||
// Get user organization role
|
||||
hybridRouter.get(
|
||||
"/user/:userId/org/:orgId/session/:sessionId/verify",
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const parsedParams = getUserOrgSessionVerifySchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { userId, orgId, sessionId } = parsedParams.data;
|
||||
const remoteExitNode = req.remoteExitNode;
|
||||
|
||||
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Remote exit node not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"User is not authorized to access this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const accessPolicy = await checkOrgAccessPolicy({
|
||||
orgId,
|
||||
userId,
|
||||
sessionId
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: accessPolicy,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User org access policy retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to get user org role"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Check if role has access to resource
|
||||
hybridRouter.get(
|
||||
"/role/:roleId/resource/:resourceId/access",
|
||||
@@ -1238,6 +1325,70 @@ hybridRouter.get(
|
||||
}
|
||||
);
|
||||
|
||||
const asnIpLookupParamsSchema = z.object({
|
||||
ip: z.union([z.ipv4(), z.ipv6()])
|
||||
});
|
||||
hybridRouter.get(
|
||||
"/asnip/:ip",
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const parsedParams = asnIpLookupParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { ip } = parsedParams.data;
|
||||
|
||||
if (!maxmindAsnLookup) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.SERVICE_UNAVAILABLE,
|
||||
"ASNIP service is not available"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const result = maxmindAsnLookup.get(ip);
|
||||
|
||||
if (!result || !result.autonomous_system_number) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"ASNIP information not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { autonomous_system_number } = result;
|
||||
|
||||
logger.debug(
|
||||
`ASNIP lookup successful for IP ${ip}: ${autonomous_system_number}`
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: { asn: autonomous_system_number },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "GeoIP lookup successful",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to validate resource session token"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GERBIL ROUTERS
|
||||
const getConfigSchema = z.object({
|
||||
publicKey: z.string(),
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as auth from "#private/routers/auth";
|
||||
import * as orgIdp from "#private/routers/orgIdp";
|
||||
import * as billing from "#private/routers/billing";
|
||||
import * as license from "#private/routers/license";
|
||||
import * as resource from "#private/routers/resource";
|
||||
|
||||
import { verifySessionUserMiddleware } from "@server/middlewares";
|
||||
|
||||
@@ -37,3 +38,5 @@ internalRouter.post(
|
||||
);
|
||||
|
||||
internalRouter.get(`/license/status`, license.getLicenseStatus);
|
||||
|
||||
internalRouter.get("/maintenance/info", resource.getMaintenanceInfo);
|
||||
|
||||
113
server/private/routers/resource/getMaintenanceInfo.ts
Normal file
113
server/private/routers/resource/getMaintenanceInfo.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 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 { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { GetMaintenanceInfoResponse } from "@server/routers/resource/types";
|
||||
|
||||
const getMaintenanceInfoSchema = z
|
||||
.object({
|
||||
fullDomain: z.string().min(1, "Domain is required")
|
||||
})
|
||||
.strict();
|
||||
|
||||
async function query(fullDomain: string) {
|
||||
const [res] = await db
|
||||
.select({
|
||||
resourceId: resources.resourceId,
|
||||
name: resources.name,
|
||||
fullDomain: resources.fullDomain,
|
||||
maintenanceModeEnabled: resources.maintenanceModeEnabled,
|
||||
maintenanceModeType: resources.maintenanceModeType,
|
||||
maintenanceTitle: resources.maintenanceTitle,
|
||||
maintenanceMessage: resources.maintenanceMessage,
|
||||
maintenanceEstimatedTime: resources.maintenanceEstimatedTime
|
||||
})
|
||||
.from(resources)
|
||||
.where(eq(resources.fullDomain, fullDomain))
|
||||
.limit(1);
|
||||
return res;
|
||||
}
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/maintenance/info",
|
||||
description: "Get maintenance information for a resource by domain.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
query: z.object({
|
||||
fullDomain: z.string()
|
||||
})
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Maintenance information retrieved successfully"
|
||||
},
|
||||
404: {
|
||||
description: "Resource not found"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export async function getMaintenanceInfo(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = getMaintenanceInfoSchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { fullDomain } = parsedQuery.data;
|
||||
|
||||
const maintenanceInfo = await query(fullDomain);
|
||||
|
||||
if (!maintenanceInfo) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||
);
|
||||
}
|
||||
|
||||
return response<GetMaintenanceInfoResponse>(res, {
|
||||
data: maintenanceInfo,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Maintenance information retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"An error occurred while retrieving maintenance information"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
14
server/private/routers/resource/index.ts
Normal file
14
server/private/routers/resource/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025 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.
|
||||
*/
|
||||
|
||||
export * from "./getMaintenanceInfo";
|
||||
Reference in New Issue
Block a user