mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-15 06:16:37 +00:00
Merge branch 'dev' into private-http-ha
This commit is contained in:
@@ -440,6 +440,12 @@ authenticated.get(
|
||||
resource.getUserResources
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/user-resource-aliases",
|
||||
verifyOrgAccess,
|
||||
resource.listUserResourceAliases
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/domains",
|
||||
verifyOrgAccess,
|
||||
|
||||
@@ -173,13 +173,13 @@ export async function buildClientConfigurationForNewtClient(
|
||||
)
|
||||
);
|
||||
|
||||
const resourceTarget = await generateSubnetProxyTargetV2(
|
||||
const resourceTargets = await generateSubnetProxyTargetV2(
|
||||
resource,
|
||||
resourceClients
|
||||
);
|
||||
|
||||
if (resourceTarget) {
|
||||
targetsToSend.push(resourceTarget);
|
||||
if (resourceTargets) {
|
||||
targetsToSend.push(...resourceTargets);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ async function createHttpResource(
|
||||
|
||||
if (build == "saas" && !isSubscribed(orgId!, tierMatrix.domainNamespaces)) {
|
||||
// grandfather in existing users
|
||||
const lastAllowedDate = new Date("2026-04-12");
|
||||
const lastAllowedDate = new Date("2026-04-13");
|
||||
const userCreatedDate = new Date(req.user?.dateCreated || new Date());
|
||||
if (userCreatedDate > lastAllowedDate) {
|
||||
// check if this domain id is a namespace domain and if so, reject
|
||||
|
||||
@@ -142,6 +142,7 @@ export async function getUserResources(
|
||||
let siteResourcesData: Array<{
|
||||
siteResourceId: number;
|
||||
name: string;
|
||||
niceId: string;
|
||||
destination: string;
|
||||
mode: string;
|
||||
scheme: string | null;
|
||||
@@ -154,6 +155,7 @@ export async function getUserResources(
|
||||
.select({
|
||||
siteResourceId: siteResources.siteResourceId,
|
||||
name: siteResources.name,
|
||||
niceId: siteResources.niceId,
|
||||
destination: siteResources.destination,
|
||||
mode: siteResources.mode,
|
||||
scheme: siteResources.scheme,
|
||||
@@ -249,7 +251,7 @@ export async function getUserResources(
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
data: {
|
||||
resources: resourcesWithAuth,
|
||||
siteResources: siteResourcesFormatted
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ export * from "./deleteResourceRule";
|
||||
export * from "./listResourceRules";
|
||||
export * from "./updateResourceRule";
|
||||
export * from "./getUserResources";
|
||||
export * from "./listUserResourceAliases";
|
||||
export * from "./setResourceHeaderAuth";
|
||||
export * from "./addEmailToResourceWhitelist";
|
||||
export * from "./removeEmailFromResourceWhitelist";
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
resourcePincode,
|
||||
resources,
|
||||
roleResources,
|
||||
sites,
|
||||
targetHealthCheck,
|
||||
targets,
|
||||
userResources
|
||||
@@ -138,6 +139,7 @@ export type ResourceWithTargets = {
|
||||
port: number;
|
||||
enabled: boolean;
|
||||
healthStatus: "healthy" | "unhealthy" | "unknown" | null;
|
||||
siteName: string | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
@@ -446,14 +448,16 @@ export async function listResources(
|
||||
port: targets.port,
|
||||
enabled: targets.enabled,
|
||||
healthStatus: targetHealthCheck.hcHealth,
|
||||
hcEnabled: targetHealthCheck.hcEnabled
|
||||
hcEnabled: targetHealthCheck.hcEnabled,
|
||||
siteName: sites.name
|
||||
})
|
||||
.from(targets)
|
||||
.where(inArray(targets.resourceId, resourceIdList))
|
||||
.leftJoin(
|
||||
targetHealthCheck,
|
||||
eq(targetHealthCheck.targetId, targets.targetId)
|
||||
);
|
||||
)
|
||||
.leftJoin(sites, eq(targets.siteId, sites.siteId));
|
||||
|
||||
// avoids TS issues with reduce/never[]
|
||||
const map = new Map<number, ResourceWithTargets>();
|
||||
|
||||
262
server/routers/resource/listUserResourceAliases.ts
Normal file
262
server/routers/resource/listUserResourceAliases.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import {
|
||||
db,
|
||||
siteResources,
|
||||
userSiteResources,
|
||||
roleSiteResources,
|
||||
userOrgRoles,
|
||||
userOrgs
|
||||
} from "@server/db";
|
||||
import { and, eq, inArray, asc, isNotNull, ne } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { localCache } from "#dynamic/lib/cache";
|
||||
|
||||
const USER_RESOURCE_ALIASES_CACHE_TTL_SEC = 60;
|
||||
|
||||
function userResourceAliasesCacheKey(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
page: number,
|
||||
pageSize: number
|
||||
) {
|
||||
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}`;
|
||||
}
|
||||
|
||||
const listUserResourceAliasesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
const listUserResourceAliasesQuerySchema = z.object({
|
||||
pageSize: z.coerce
|
||||
.number<string>()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(20)
|
||||
.default(20)
|
||||
.openapi({
|
||||
type: "integer",
|
||||
default: 20,
|
||||
description: "Number of items per page"
|
||||
}),
|
||||
page: z.coerce
|
||||
.number<string>()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.catch(1)
|
||||
.default(1)
|
||||
.openapi({
|
||||
type: "integer",
|
||||
default: 1,
|
||||
description: "Page number to retrieve"
|
||||
})
|
||||
});
|
||||
|
||||
export type ListUserResourceAliasesResponse = PaginatedResponse<{
|
||||
aliases: string[];
|
||||
}>;
|
||||
|
||||
// registry.registerPath({
|
||||
// method: "get",
|
||||
// path: "/org/{orgId}/user-resource-aliases",
|
||||
// description:
|
||||
// "List private (host-mode) site resource aliases the authenticated user can access in the organization, paginated.",
|
||||
// tags: [OpenAPITags.PrivateResource],
|
||||
// request: {
|
||||
// params: z.object({
|
||||
// orgId: z.string()
|
||||
// }),
|
||||
// query: listUserResourceAliasesQuerySchema
|
||||
// },
|
||||
// responses: {}
|
||||
// });
|
||||
|
||||
export async function listUserResourceAliases(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = listUserResourceAliasesQuerySchema.safeParse(
|
||||
req.query
|
||||
);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const { page, pageSize } = parsedQuery.data;
|
||||
|
||||
const parsedParams = listUserResourceAliasesParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
const userId = req.user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||
);
|
||||
}
|
||||
|
||||
const [userOrg] = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||
.limit(1);
|
||||
|
||||
if (!userOrg) {
|
||||
return next(
|
||||
createHttpError(HttpCode.FORBIDDEN, "User not in organization")
|
||||
);
|
||||
}
|
||||
|
||||
const cacheKey = userResourceAliasesCacheKey(
|
||||
orgId,
|
||||
userId,
|
||||
page,
|
||||
pageSize
|
||||
);
|
||||
const cachedData: ListUserResourceAliasesResponse | undefined =
|
||||
localCache.get(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
return response<ListUserResourceAliasesResponse>(res, {
|
||||
data: cachedData,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User resource aliases retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
const userRoleIds = await db
|
||||
.select({ roleId: userOrgRoles.roleId })
|
||||
.from(userOrgRoles)
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgRoles.userId, userId),
|
||||
eq(userOrgRoles.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.map((r) => r.roleId));
|
||||
|
||||
const directSiteResourcesQuery = db
|
||||
.select({ siteResourceId: userSiteResources.siteResourceId })
|
||||
.from(userSiteResources)
|
||||
.where(eq(userSiteResources.userId, userId));
|
||||
|
||||
const roleSiteResourcesQuery =
|
||||
userRoleIds.length > 0
|
||||
? db
|
||||
.select({
|
||||
siteResourceId: roleSiteResources.siteResourceId
|
||||
})
|
||||
.from(roleSiteResources)
|
||||
.where(inArray(roleSiteResources.roleId, userRoleIds))
|
||||
: Promise.resolve([]);
|
||||
|
||||
const [directSiteResourceResults, roleSiteResourceResults] =
|
||||
await Promise.all([
|
||||
directSiteResourcesQuery,
|
||||
roleSiteResourcesQuery
|
||||
]);
|
||||
|
||||
const accessibleSiteResourceIds = [
|
||||
...directSiteResourceResults.map((r) => r.siteResourceId),
|
||||
...roleSiteResourceResults.map((r) => r.siteResourceId)
|
||||
];
|
||||
|
||||
if (accessibleSiteResourceIds.length === 0) {
|
||||
const data: ListUserResourceAliasesResponse = {
|
||||
aliases: [],
|
||||
pagination: {
|
||||
total: 0,
|
||||
pageSize,
|
||||
page
|
||||
}
|
||||
};
|
||||
localCache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC);
|
||||
return response<ListUserResourceAliasesResponse>(res, {
|
||||
data,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User resource aliases retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
const whereClause = and(
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true),
|
||||
eq(siteResources.mode, "host"),
|
||||
isNotNull(siteResources.alias),
|
||||
ne(siteResources.alias, ""),
|
||||
inArray(siteResources.siteResourceId, accessibleSiteResourceIds)
|
||||
);
|
||||
|
||||
const baseSelect = () =>
|
||||
db
|
||||
.select({ alias: siteResources.alias })
|
||||
.from(siteResources)
|
||||
.where(whereClause);
|
||||
|
||||
const countQuery = db.$count(baseSelect().as("filtered_aliases"));
|
||||
|
||||
const [rows, totalCount] = await Promise.all([
|
||||
baseSelect()
|
||||
.orderBy(asc(siteResources.alias))
|
||||
.limit(pageSize)
|
||||
.offset(pageSize * (page - 1)),
|
||||
countQuery
|
||||
]);
|
||||
|
||||
const aliases = rows.map((r) => r.alias as string);
|
||||
|
||||
const data: ListUserResourceAliasesResponse = {
|
||||
aliases,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
pageSize,
|
||||
page
|
||||
}
|
||||
};
|
||||
localCache.set(cacheKey, data, USER_RESOURCE_ALIASES_CACHE_TTL_SEC);
|
||||
|
||||
return response<ListUserResourceAliasesResponse>(res, {
|
||||
data,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User resource aliases retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -326,7 +326,7 @@ async function updateHttpResource(
|
||||
!isSubscribed(resource.orgId, tierMatrix.domainNamespaces)
|
||||
) {
|
||||
// grandfather in existing users
|
||||
const lastAllowedDate = new Date("2026-04-12");
|
||||
const lastAllowedDate = new Date("2026-04-13");
|
||||
const userCreatedDate = new Date(
|
||||
req.user?.dateCreated || new Date()
|
||||
);
|
||||
|
||||
@@ -768,11 +768,11 @@ export async function handleMessagingForUpdatedSiteResource(
|
||||
portRangesChanged ||
|
||||
destinationPortChanged
|
||||
) {
|
||||
const oldTarget = await generateSubnetProxyTargetV2(
|
||||
const oldTargets = await generateSubnetProxyTargetV2(
|
||||
existingSiteResource,
|
||||
mergedAllClients
|
||||
);
|
||||
const newTarget = await generateSubnetProxyTargetV2(
|
||||
const newTargets = await generateSubnetProxyTargetV2(
|
||||
updatedSiteResource,
|
||||
mergedAllClients
|
||||
);
|
||||
@@ -780,8 +780,8 @@ export async function handleMessagingForUpdatedSiteResource(
|
||||
await updateTargets(
|
||||
newt.newtId,
|
||||
{
|
||||
oldTargets: oldTarget ? [oldTarget] : [],
|
||||
newTargets: newTarget ? [newTarget] : []
|
||||
oldTargets: oldTargets ? oldTargets : [],
|
||||
newTargets: newTargets ? newTargets : []
|
||||
},
|
||||
newt.version
|
||||
);
|
||||
|
||||
@@ -21,7 +21,8 @@ async function queryUser(userId: string) {
|
||||
serverAdmin: users.serverAdmin,
|
||||
idpName: idp.name,
|
||||
idpId: users.idpId,
|
||||
locale: users.locale
|
||||
locale: users.locale,
|
||||
dateCreated: users.dateCreated
|
||||
})
|
||||
.from(users)
|
||||
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||
|
||||
@@ -64,7 +64,8 @@ export async function myDevice(
|
||||
serverAdmin: users.serverAdmin,
|
||||
idpName: idp.name,
|
||||
idpId: users.idpId,
|
||||
locale: users.locale
|
||||
locale: users.locale,
|
||||
dateCreated: users.dateCreated
|
||||
})
|
||||
.from(users)
|
||||
.leftJoin(idp, eq(users.idpId, idp.idpId))
|
||||
|
||||
Reference in New Issue
Block a user