Chungus 2.0

This commit is contained in:
Owen
2025-10-10 11:27:15 -07:00
parent f64a477c3d
commit d92b87b7c8
224 changed files with 1507 additions and 1586 deletions

View File

@@ -1,105 +0,0 @@
/*
* 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 cors, { CorsOptions } from "cors";
import config from "@server/lib/config";
import logger from "@server/logger";
import { db, loginPage } from "@server/db";
import { eq } from "drizzle-orm";
async function isValidLoginPageDomain(host: string): Promise<boolean> {
try {
const [result] = await db
.select()
.from(loginPage)
.where(eq(loginPage.fullDomain, host))
.limit(1);
const isValid = !!result;
return isValid;
} catch (error) {
logger.error("Error checking loginPage domain:", error);
return false;
}
}
export function corsWithLoginPageSupport(corsConfig: any) {
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)
};
return async (req: Request, res: Response, next: NextFunction) => {
const originValidatedCorsConfig = {
origin: async (
origin: string | undefined,
callback: (err: Error | null, allow?: boolean) => void
) => {
// If no origin (e.g., same-origin request), allow it
if (!origin) {
return callback(null, true);
}
const dashboardUrl = config.getRawConfig().app.dashboard_url;
// If no dashboard_url is configured, allow all origins
if (!dashboardUrl) {
return callback(null, true);
}
// Check if origin matches dashboard URL
const dashboardHost = new URL(dashboardUrl).host;
const originHost = new URL(origin).host;
if (originHost === dashboardHost) {
return callback(null, true);
}
if (
corsConfig?.origins &&
corsConfig.origins.includes(origin)
) {
return callback(null, true);
}
// If origin doesn't match dashboard URL, check if it's a valid loginPage domain
const isValidDomain = await isValidLoginPageDomain(originHost);
if (isValidDomain) {
return callback(null, true);
}
// Origin is not valid
return callback(null, false);
},
methods: corsConfig?.methods,
allowedHeaders: corsConfig?.allowed_headers,
credentials: corsConfig?.credentials !== false
} as CorsOptions;
return cors(originValidatedCorsConfig)(req, res, next);
};
}

View File

@@ -1,18 +0,0 @@
/*
* 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 "./verifyCertificateAccess";
export * from "./verifyRemoteExitNodeAccess";
export * from "./verifyIdpAccess";
export * from "./verifyLoginPageAccess";
export * from "./corsWithLoginPage";

View File

@@ -1,126 +0,0 @@
/*
* 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 { db, domainNamespaces } from "@server/db";
import { certificates } from "@server/db";
import { domains, orgDomains } from "@server/db";
import { eq, and } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
export async function verifyCertificateAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
// Assume user/org access is already verified
const orgId = req.params.orgId;
const certId = req.params.certId || req.body?.certId || req.query?.certId;
let domainId =
req.params.domainId || req.body?.domainId || req.query?.domainId;
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (!domainId) {
if (!certId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Must provide either certId or domainId")
);
}
// Get the certificate and its domainId
const [cert] = await db
.select()
.from(certificates)
.where(eq(certificates.certId, Number(certId)))
.limit(1);
if (!cert) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Certificate with ID ${certId} not found`
)
);
}
domainId = cert.domainId;
if (!domainId) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Certificate with ID ${certId} does not have a domain`
)
);
}
}
if (!domainId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Must provide either certId or domainId")
);
}
// Check if the domain is a namespace domain
const [namespaceDomain] = await db
.select()
.from(domainNamespaces)
.where(eq(domainNamespaces.domainId, domainId))
.limit(1);
if (namespaceDomain) {
// If it's a namespace domain, we can skip the org check
return next();
}
// Check if the domain is associated with the org
const [orgDomain] = await db
.select()
.from(orgDomains)
.where(
and(
eq(orgDomains.orgId, orgId),
eq(orgDomains.domainId, domainId)
)
)
.limit(1);
if (!orgDomain) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Organization does not have access to this certificate"
)
);
}
return next();
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying certificate access"
)
);
}
}
export default verifyCertificateAccess;

View File

@@ -1,102 +0,0 @@
/*
* 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 { userOrgs, db, idp, idpOrg } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyIdpAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const userId = req.user!.userId;
const idpId =
req.params.idpId || req.body.idpId || req.query.idpId;
const orgId = req.params.orgId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (!idpId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
);
}
const [idpRes] = await db
.select()
.from(idp)
.innerJoin(idpOrg, eq(idp.idpId, idpOrg.idpId))
.where(
and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId))
)
.limit(1);
if (!idpRes || !idpRes.idp || !idpRes.idpOrg) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`IdP with ID ${idpId} not found for organization ${orgId}`
)
);
}
if (!req.userOrg) {
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, idpRes.idpOrg.orgId)
)
)
.limit(1);
req.userOrg = userOrgRole[0];
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying idp access"
)
);
}
}

View File

@@ -1,81 +0,0 @@
/*
* 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 { userOrgs, db, loginPageOrg } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyLoginPageAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const userId = req.user!.userId;
const loginPageId =
req.params.loginPageId ||
req.body.loginPageId ||
req.query.loginPageId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!loginPageId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid login page ID")
);
}
const loginPageOrgs = await db
.select({
orgId: loginPageOrg.orgId
})
.from(loginPageOrg)
.where(eq(loginPageOrg.loginPageId, loginPageId));
const orgIds = loginPageOrgs.map((lpo) => lpo.orgId);
const existingUserOrgs = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
inArray(userOrgs.orgId, orgIds)
)
);
if (existingUserOrgs.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Login page with ID ${loginPageId} not found for user's organizations`
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying login page access"
)
);
}
}

View File

@@ -1,56 +0,0 @@
/*
* 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 { NextFunction, Response } from "express";
import ErrorResponse from "@server/types/ErrorResponse";
import config from "@server/lib/config";
import { unauthorized } from "@server/auth/unauthorizedResponse";
import logger from "@server/logger";
import { validateRemoteExitNodeSessionToken } from "@server/auth/sessions/privateRemoteExitNode";
export const verifySessionRemoteExitNodeMiddleware = async (
req: any,
res: Response<ErrorResponse>,
next: NextFunction
) => {
// get the token from the auth header
const token = req.headers["authorization"]?.split(" ")[1] || "";
const { session, remoteExitNode } = await validateRemoteExitNodeSessionToken(token);
if (!session || !remoteExitNode) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(`Remote exit node session not found. IP: ${req.ip}.`);
}
return next(unauthorized());
}
// const existingUser = await db
// .select()
// .from(users)
// .where(eq(users.userId, user.userId));
// if (!existingUser || !existingUser[0]) {
// if (config.getRawConfig().app.log_failed_attempts) {
// logger.info(`User session not found. IP: ${req.ip}.`);
// }
// return next(
// createHttpError(HttpCode.BAD_REQUEST, "User does not exist")
// );
// }
req.session = session;
req.remoteExitNode = remoteExitNode;
next();
};

View File

@@ -1,118 +0,0 @@
/*
* 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 { db, exitNodeOrgs, exitNodes, remoteExitNodes } from "@server/db";
import { sites, userOrgs, userSites, roleSites, roles } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyRemoteExitNodeAccess(
req: Request,
res: Response,
next: NextFunction
) {
const userId = req.user!.userId; // Assuming you have user information in the request
const orgId = req.params.orgId;
const remoteExitNodeId =
req.params.remoteExitNodeId ||
req.body.remoteExitNodeId ||
req.query.remoteExitNodeId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
try {
const [remoteExitNode] = await db
.select()
.from(remoteExitNodes)
.where(and(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId)));
if (!remoteExitNode) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Remote exit node with ID ${remoteExitNodeId} not found`
)
);
}
if (!remoteExitNode.exitNodeId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Remote exit node with ID ${remoteExitNodeId} does not have an exit node ID`
)
);
}
const [exitNodeOrg] = await db
.select()
.from(exitNodeOrgs)
.where(
and(
eq(exitNodeOrgs.exitNodeId, remoteExitNode.exitNodeId),
eq(exitNodeOrgs.orgId, orgId)
)
);
if (!exitNodeOrg) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Remote exit node with ID ${remoteExitNodeId} not found in organization ${orgId}`
)
);
}
if (!req.userOrg) {
// Get user's role ID in the organization
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, exitNodeOrg.orgId)
)
)
.limit(1);
req.userOrg = userOrgRole[0];
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying remote exit node access"
)
);
}
}