mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-06 04:10:13 +00:00
Merge pull request #3375 from fosrl/resource-launcher
Resource launcher
This commit is contained in:
@@ -179,7 +179,8 @@ export enum ActionsEnum {
|
||||
setResourcePolicyPincode = "setResourcePolicyPincode",
|
||||
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
||||
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
||||
setResourcePolicyRules = "setResourcePolicyRules"
|
||||
setResourcePolicyRules = "setResourcePolicyRules",
|
||||
createOrgWideLauncherView = "createOrgWideLauncherView"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
||||
@@ -222,6 +222,20 @@ export const labels = pgTable("labels", {
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const launcherViews = pgTable("launcherViews", {
|
||||
viewId: serial("viewId").primaryKey(),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
userId: varchar("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
name: varchar("name").notNull(),
|
||||
config: text("config").notNull(),
|
||||
createdAt: varchar("createdAt").notNull(),
|
||||
updatedAt: varchar("updatedAt").notNull()
|
||||
});
|
||||
|
||||
export const siteLabels = pgTable(
|
||||
"siteLabels",
|
||||
{
|
||||
@@ -1575,6 +1589,7 @@ export type RoundTripMessageTracker = InferSelectModel<
|
||||
export type Network = InferSelectModel<typeof networks>;
|
||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||
export type Label = InferSelectModel<typeof labels>;
|
||||
export type LauncherView = InferSelectModel<typeof launcherViews>;
|
||||
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
||||
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
||||
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
||||
|
||||
@@ -223,6 +223,20 @@ export const labels = sqliteTable("labels", {
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const launcherViews = sqliteTable("launcherViews", {
|
||||
viewId: integer("viewId").primaryKey({ autoIncrement: true }),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
userId: text("userId").references(() => users.userId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
name: text("name").notNull(),
|
||||
config: text("config").notNull(),
|
||||
createdAt: text("createdAt").notNull(),
|
||||
updatedAt: text("updatedAt").notNull()
|
||||
});
|
||||
|
||||
export const siteLabels = sqliteTable(
|
||||
"siteLabels",
|
||||
{
|
||||
@@ -1551,6 +1565,7 @@ export type RoundTripMessageTracker = InferSelectModel<
|
||||
>;
|
||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||
export type Label = InferSelectModel<typeof labels>;
|
||||
export type LauncherView = InferSelectModel<typeof launcherViews>;
|
||||
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
||||
export type ResourcePolicyPincode = InferSelectModel<
|
||||
typeof resourcePolicyPincode
|
||||
|
||||
@@ -17,6 +17,7 @@ import * as idp from "./idp";
|
||||
import * as blueprints from "./blueprints";
|
||||
import * as apiKeys from "./apiKeys";
|
||||
import * as logs from "./auditLogs";
|
||||
import * as launcher from "./launcher";
|
||||
import * as newt from "./newt";
|
||||
import * as olm from "./olm";
|
||||
import * as serverInfo from "./serverInfo";
|
||||
@@ -463,6 +464,54 @@ authenticated.get(
|
||||
resource.getUserResources
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/groups",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherGroups
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/resources",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherResources
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/sites",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherSites
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/labels",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherLabels
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/views",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherViews
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/launcher/views",
|
||||
verifyOrgAccess,
|
||||
launcher.createLauncherView
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/launcher/views/:viewId",
|
||||
verifyOrgAccess,
|
||||
launcher.updateLauncherView
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId/launcher/views/:viewId",
|
||||
verifyOrgAccess,
|
||||
launcher.deleteLauncherView
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/user-resource-aliases",
|
||||
verifyOrgAccess,
|
||||
|
||||
@@ -159,6 +159,7 @@ authenticated.get(
|
||||
verifyApiKeyOrgAccess,
|
||||
resource.getUserResources
|
||||
);
|
||||
|
||||
// Site Resource endpoints
|
||||
authenticated.put(
|
||||
"/org/:orgId/site-resource",
|
||||
|
||||
101
server/routers/launcher/createLauncherView.ts
Normal file
101
server/routers/launcher/createLauncherView.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { db, launcherViews } from "@server/db";
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import moment from "moment";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { launcherViewConfigSchema } from "./types";
|
||||
|
||||
const createLauncherViewBodySchema = z.strictObject({
|
||||
name: z.string().min(1).max(128),
|
||||
config: launcherViewConfigSchema,
|
||||
orgWide: z.boolean().optional().default(false)
|
||||
});
|
||||
|
||||
export async function createLauncherView(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = createLauncherViewBodySchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.data.orgWide) {
|
||||
const canCreateOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
req
|
||||
);
|
||||
if (!canCreateOrgWide) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const now = moment().toISOString();
|
||||
const [created] = await db
|
||||
.insert(launcherViews)
|
||||
.values({
|
||||
orgId,
|
||||
userId: parsed.data.orgWide ? null : userId,
|
||||
name: parsed.data.name,
|
||||
config: JSON.stringify(parsed.data.config),
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
})
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
viewId: created.viewId,
|
||||
orgId: created.orgId,
|
||||
userId: created.userId,
|
||||
name: created.name,
|
||||
config: launcherViewConfigSchema.parse(
|
||||
JSON.parse(created.config)
|
||||
),
|
||||
createdAt: created.createdAt,
|
||||
updatedAt: created.updatedAt,
|
||||
isOrgWide: created.userId == null
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher view created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error creating launcher view:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
86
server/routers/launcher/deleteLauncherView.ts
Normal file
86
server/routers/launcher/deleteLauncherView.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { db, launcherViews } from "@server/db";
|
||||
import { response } from "@server/lib/response";
|
||||
import { getFirstString } from "@server/lib/requestParams";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
|
||||
export async function deleteLauncherView(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
const viewId = Number.parseInt(
|
||||
getFirstString(req.params.viewId) ?? "",
|
||||
10
|
||||
);
|
||||
|
||||
if (!orgId || !Number.isFinite(viewId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid request parameters"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(launcherViews)
|
||||
.where(
|
||||
and(
|
||||
eq(launcherViews.viewId, viewId),
|
||||
eq(launcherViews.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Launcher view not found")
|
||||
);
|
||||
}
|
||||
|
||||
const isPersonalView = existing.userId === userId;
|
||||
const isOrgWideView = existing.userId == null;
|
||||
const canManageOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
req
|
||||
);
|
||||
|
||||
if (!isPersonalView && !(isOrgWideView && canManageOrgWide)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"You do not have permission to delete this view"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.delete(launcherViews).where(eq(launcherViews.viewId, viewId));
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher view deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error deleting launcher view:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
172
server/routers/launcher/formatLauncherAccess.ts
Normal file
172
server/routers/launcher/formatLauncherAccess.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { formatEndpoint, parseEndpoint } from "@server/lib/ip";
|
||||
|
||||
export type SiteResourceDestinationInput = {
|
||||
mode: "host" | "cidr" | "http" | "ssh";
|
||||
destination: string | null;
|
||||
destinationPort: number | null;
|
||||
scheme: "http" | "https" | null;
|
||||
};
|
||||
|
||||
export function resolveHttpHttpsDisplayPort(
|
||||
mode: "http",
|
||||
destinationPort: number | null
|
||||
): number {
|
||||
if (destinationPort != null) {
|
||||
return destinationPort;
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
|
||||
export function formatSiteResourceDestinationDisplay(
|
||||
row: SiteResourceDestinationInput
|
||||
): string {
|
||||
if (!row.destination) {
|
||||
return "";
|
||||
}
|
||||
const { mode, destination, destinationPort, scheme } = row;
|
||||
if (mode !== "http") {
|
||||
return destination;
|
||||
}
|
||||
const port = resolveHttpHttpsDisplayPort(mode, destinationPort);
|
||||
const downstreamScheme = scheme ?? "http";
|
||||
const hostPart =
|
||||
destination.includes(":") && !destination.startsWith("[")
|
||||
? `[${destination}]`
|
||||
: destination;
|
||||
return `${downstreamScheme}://${hostPart}:${port}`;
|
||||
}
|
||||
|
||||
export type PublicResourceAccessInput = {
|
||||
mode: string;
|
||||
fullDomain: string | null;
|
||||
ssl: boolean;
|
||||
proxyPort: number | null;
|
||||
wildcard: boolean;
|
||||
exitNodeEndpoint?: string | null;
|
||||
};
|
||||
|
||||
export type SiteResourceAccessInput = {
|
||||
mode: string;
|
||||
destination: string | null;
|
||||
destinationPort: number | null;
|
||||
scheme: "http" | "https" | null;
|
||||
ssl: boolean;
|
||||
fullDomain: string | null;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
};
|
||||
|
||||
export type LauncherAccessFields = {
|
||||
accessDisplay: string;
|
||||
accessCopyValue: string;
|
||||
accessUrl: string | null;
|
||||
};
|
||||
|
||||
function formatTcpUdpResourceAccess(
|
||||
exitNodeEndpoint: string | null | undefined,
|
||||
proxyPort: number | null
|
||||
): LauncherAccessFields {
|
||||
if (proxyPort == null) {
|
||||
return {
|
||||
accessDisplay: "",
|
||||
accessCopyValue: "",
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
if (!exitNodeEndpoint?.trim()) {
|
||||
const port = proxyPort.toString();
|
||||
return {
|
||||
accessDisplay: port,
|
||||
accessCopyValue: port,
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = parseEndpoint(exitNodeEndpoint);
|
||||
const host = parsed?.ip ?? exitNodeEndpoint.trim();
|
||||
const access = formatEndpoint(host, proxyPort);
|
||||
|
||||
return {
|
||||
accessDisplay: access,
|
||||
accessCopyValue: access,
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
export function formatPublicResourceAccess(
|
||||
resource: PublicResourceAccessInput
|
||||
): LauncherAccessFields {
|
||||
const browserModes = ["http", "ssh", "rdp", "vnc"];
|
||||
if (!browserModes.includes(resource.mode)) {
|
||||
return formatTcpUdpResourceAccess(
|
||||
resource.exitNodeEndpoint,
|
||||
resource.proxyPort
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.fullDomain) {
|
||||
return {
|
||||
accessDisplay: "",
|
||||
accessCopyValue: "",
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
||||
return {
|
||||
accessDisplay: url,
|
||||
accessCopyValue: url,
|
||||
accessUrl: resource.wildcard ? null : url
|
||||
};
|
||||
}
|
||||
|
||||
export function formatSiteResourceAccess(
|
||||
resource: SiteResourceAccessInput
|
||||
): LauncherAccessFields {
|
||||
if (resource.alias) {
|
||||
return {
|
||||
accessDisplay: resource.alias,
|
||||
accessCopyValue: resource.alias,
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
if (resource.mode === "http" && resource.fullDomain) {
|
||||
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
||||
return {
|
||||
accessDisplay: url,
|
||||
accessCopyValue: url,
|
||||
accessUrl: url
|
||||
};
|
||||
}
|
||||
|
||||
const destination = formatSiteResourceDestinationDisplay({
|
||||
mode: resource.mode as SiteResourceDestinationInput["mode"],
|
||||
destination: resource.destination,
|
||||
destinationPort: resource.destinationPort,
|
||||
scheme: resource.scheme
|
||||
});
|
||||
|
||||
if (destination) {
|
||||
return {
|
||||
accessDisplay: destination,
|
||||
accessCopyValue: destination,
|
||||
accessUrl: resource.mode === "http" ? destination : null
|
||||
};
|
||||
}
|
||||
|
||||
if (resource.aliasAddress) {
|
||||
return {
|
||||
accessDisplay: resource.aliasAddress,
|
||||
accessCopyValue: resource.aliasAddress,
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accessDisplay: "",
|
||||
accessCopyValue: "",
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
9
server/routers/launcher/index.ts
Normal file
9
server/routers/launcher/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from "./types";
|
||||
export { listLauncherGroups } from "./listLauncherGroups";
|
||||
export { listLauncherResources } from "./listLauncherResources";
|
||||
export { listLauncherSites } from "./listLauncherSites";
|
||||
export { listLauncherLabels } from "./listLauncherLabels";
|
||||
export { listLauncherViews } from "./listLauncherViews";
|
||||
export { createLauncherView } from "./createLauncherView";
|
||||
export { updateLauncherView } from "./updateLauncherView";
|
||||
export { deleteLauncherView } from "./deleteLauncherView";
|
||||
1436
server/routers/launcher/launcherResourceAccess.ts
Normal file
1436
server/routers/launcher/launcherResourceAccess.ts
Normal file
File diff suppressed because it is too large
Load Diff
67
server/routers/launcher/listLauncherGroups.ts
Normal file
67
server/routers/launcher/listLauncherGroups.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { listLauncherGroupsForUser } from "./launcherResourceAccess";
|
||||
import { launcherListQuerySchema } from "./types";
|
||||
|
||||
export async function listLauncherGroups(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = launcherListQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { groups, total } = await listLauncherGroupsForUser(
|
||||
orgId,
|
||||
userId,
|
||||
req.userOrgRoleIds ?? [],
|
||||
parsed.data
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
groups,
|
||||
pagination: {
|
||||
total,
|
||||
page: parsed.data.page,
|
||||
pageSize: parsed.data.pageSize
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher groups retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher groups:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
67
server/routers/launcher/listLauncherLabels.ts
Normal file
67
server/routers/launcher/listLauncherLabels.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { listAccessibleLauncherLabelsForUser } from "./launcherResourceAccess";
|
||||
import { launcherFilterListQuerySchema } from "./types";
|
||||
|
||||
export async function listLauncherLabels(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = launcherFilterListQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { labels, total } = await listAccessibleLauncherLabelsForUser(
|
||||
orgId,
|
||||
userId,
|
||||
req.userOrgRoleIds ?? [],
|
||||
parsed.data
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
labels,
|
||||
pagination: {
|
||||
total,
|
||||
page: parsed.data.page,
|
||||
pageSize: parsed.data.pageSize
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher labels retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher labels:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
72
server/routers/launcher/listLauncherResources.ts
Normal file
72
server/routers/launcher/listLauncherResources.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { listLauncherResourcesForUser } from "./launcherResourceAccess";
|
||||
import { launcherListQuerySchema } from "./types";
|
||||
|
||||
const listLauncherResourcesQuerySchema = launcherListQuerySchema.extend({
|
||||
groupKey: z.string().min(1)
|
||||
});
|
||||
|
||||
export async function listLauncherResources(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = listLauncherResourcesQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resources, total } = await listLauncherResourcesForUser(
|
||||
orgId,
|
||||
userId,
|
||||
req.userOrgRoleIds ?? [],
|
||||
parsed.data
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
resources,
|
||||
pagination: {
|
||||
total,
|
||||
page: parsed.data.page,
|
||||
pageSize: parsed.data.pageSize
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher resources retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher resources:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
67
server/routers/launcher/listLauncherSites.ts
Normal file
67
server/routers/launcher/listLauncherSites.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { listAccessibleLauncherSitesForUser } from "./launcherResourceAccess";
|
||||
import { launcherFilterListQuerySchema } from "./types";
|
||||
|
||||
export async function listLauncherSites(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = launcherFilterListQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { sites, total } = await listAccessibleLauncherSitesForUser(
|
||||
orgId,
|
||||
userId,
|
||||
req.userOrgRoleIds ?? [],
|
||||
parsed.data
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
sites,
|
||||
pagination: {
|
||||
total,
|
||||
page: parsed.data.page,
|
||||
pageSize: parsed.data.pageSize
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher sites retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher sites:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
73
server/routers/launcher/listLauncherViews.ts
Normal file
73
server/routers/launcher/listLauncherViews.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { db, launcherViews } from "@server/db";
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq, isNull, or } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { launcherViewConfigSchema, type LauncherViewRecord } from "./types";
|
||||
|
||||
function mapViewRow(
|
||||
row: typeof launcherViews.$inferSelect
|
||||
): LauncherViewRecord {
|
||||
return {
|
||||
viewId: row.viewId,
|
||||
orgId: row.orgId,
|
||||
userId: row.userId,
|
||||
name: row.name,
|
||||
config: launcherViewConfigSchema.parse(JSON.parse(row.config)),
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
isOrgWide: row.userId == null
|
||||
};
|
||||
}
|
||||
|
||||
export async function listLauncherViews(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(launcherViews)
|
||||
.where(
|
||||
and(
|
||||
eq(launcherViews.orgId, orgId),
|
||||
or(
|
||||
eq(launcherViews.userId, userId),
|
||||
isNull(launcherViews.userId)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
views: rows.map(mapViewRow)
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher views retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher views:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
165
server/routers/launcher/types.ts
Normal file
165
server/routers/launcher/types.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const LAUNCHER_UNLABELED_GROUP_KEY = "unlabeled";
|
||||
export const LAUNCHER_NO_SITE_GROUP_KEY = "no-site";
|
||||
|
||||
export const launcherViewConfigSchema = z.object({
|
||||
groupBy: z.enum(["site", "label"]).default("site"),
|
||||
layout: z.enum(["grid", "list"]).default("grid"),
|
||||
sortBy: z.literal("name").default("name"),
|
||||
order: z.enum(["asc", "desc"]).default("asc"),
|
||||
showLabels: z.boolean().default(true),
|
||||
showSiteTags: z.boolean().default(true),
|
||||
showRecents: z.boolean().default(false).optional(),
|
||||
siteIds: z.array(z.number()).default([]),
|
||||
labelIds: z.array(z.number()).default([]),
|
||||
query: z.string().default("")
|
||||
});
|
||||
|
||||
export type LauncherViewConfig = z.infer<typeof launcherViewConfigSchema>;
|
||||
|
||||
export const defaultLauncherViewConfig: LauncherViewConfig =
|
||||
launcherViewConfigSchema.parse({});
|
||||
|
||||
export type LauncherLabel = {
|
||||
labelId: number;
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type LauncherSiteInfo = {
|
||||
siteId: number;
|
||||
name: string;
|
||||
type: string;
|
||||
online?: boolean;
|
||||
};
|
||||
|
||||
export type LauncherResource = {
|
||||
launcherResourceKey: string;
|
||||
resourceType: "public" | "site";
|
||||
resourceId: number;
|
||||
siteResourceId?: number;
|
||||
niceId: string;
|
||||
name: string;
|
||||
accessDisplay: string;
|
||||
accessCopyValue: string;
|
||||
accessUrl: string | null;
|
||||
iconUrl: string | null;
|
||||
enabled: boolean;
|
||||
mode: string;
|
||||
labels: LauncherLabel[];
|
||||
site?: LauncherSiteInfo;
|
||||
};
|
||||
|
||||
export type LauncherGroup = {
|
||||
groupKey: string;
|
||||
name: string;
|
||||
groupType: "site" | "label";
|
||||
itemCount: number;
|
||||
siteType?: string;
|
||||
siteOnline?: boolean;
|
||||
labelColor?: string;
|
||||
};
|
||||
|
||||
export type ListLauncherGroupsResponse = {
|
||||
groups: LauncherGroup[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ListLauncherResourcesResponse = {
|
||||
resources: LauncherResource[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type LauncherViewRecord = {
|
||||
viewId: number;
|
||||
orgId: string;
|
||||
userId: string | null;
|
||||
name: string;
|
||||
config: LauncherViewConfig;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isOrgWide: boolean;
|
||||
};
|
||||
|
||||
export type ListLauncherViewsResponse = {
|
||||
views: LauncherViewRecord[];
|
||||
};
|
||||
|
||||
export const launcherFilterListQuerySchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(500)
|
||||
.default(500),
|
||||
page: z.coerce.number().int().min(1).optional().catch(1).default(1),
|
||||
query: z.string().optional().default("")
|
||||
});
|
||||
|
||||
export type LauncherFilterListQuery = z.infer<
|
||||
typeof launcherFilterListQuerySchema
|
||||
>;
|
||||
|
||||
export type ListLauncherSitesResponse = {
|
||||
sites: LauncherSiteInfo[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ListLauncherLabelsResponse = {
|
||||
labels: LauncherLabel[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const launcherListQuerySchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(20)
|
||||
.default(20),
|
||||
page: z.coerce.number().int().min(1).optional().catch(1).default(1),
|
||||
query: z.string().optional().default(""),
|
||||
groupBy: z.enum(["site", "label"]).optional().default("site"),
|
||||
groupKey: z.string().optional(),
|
||||
siteIds: z.string().optional(),
|
||||
labelIds: z.string().optional(),
|
||||
sort_by: z.literal("name").optional().default("name"),
|
||||
order: z.enum(["asc", "desc"]).optional().default("asc")
|
||||
});
|
||||
|
||||
export type LauncherListQuery = z.infer<typeof launcherListQuerySchema>;
|
||||
|
||||
export function parseIdListParam(value: string | undefined): number[] {
|
||||
if (!value?.trim()) {
|
||||
return [];
|
||||
}
|
||||
return value
|
||||
.split(",")
|
||||
.map((part) => Number.parseInt(part.trim(), 10))
|
||||
.filter((id) => Number.isFinite(id));
|
||||
}
|
||||
|
||||
export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
|
||||
|
||||
export type LauncherViewSelection =
|
||||
| { type: "default" }
|
||||
| { type: "saved"; viewId: number };
|
||||
157
server/routers/launcher/updateLauncherView.ts
Normal file
157
server/routers/launcher/updateLauncherView.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { db, launcherViews } from "@server/db";
|
||||
import { response } from "@server/lib/response";
|
||||
import { getFirstString } from "@server/lib/requestParams";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import moment from "moment";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { launcherViewConfigSchema } from "./types";
|
||||
|
||||
const updateLauncherViewBodySchema = z.strictObject({
|
||||
name: z.string().min(1).max(128).optional(),
|
||||
config: launcherViewConfigSchema.optional(),
|
||||
orgWide: z.boolean().optional()
|
||||
});
|
||||
|
||||
export async function updateLauncherView(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
const viewId = Number.parseInt(
|
||||
getFirstString(req.params.viewId) ?? "",
|
||||
10
|
||||
);
|
||||
|
||||
if (!orgId || !Number.isFinite(viewId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid request parameters"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = updateLauncherViewBodySchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(launcherViews)
|
||||
.where(
|
||||
and(
|
||||
eq(launcherViews.viewId, viewId),
|
||||
eq(launcherViews.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Launcher view not found")
|
||||
);
|
||||
}
|
||||
|
||||
const isPersonalView = existing.userId === userId;
|
||||
const isOrgWideView = existing.userId == null;
|
||||
const canManageOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
req
|
||||
);
|
||||
|
||||
if (!isPersonalView && !(isOrgWideView && canManageOrgWide)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"You do not have permission to update this view"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.data.orgWide === true && !canManageOrgWide) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
parsed.data.orgWide === false &&
|
||||
isOrgWideView &&
|
||||
!canManageOrgWide
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const nextUserId =
|
||||
parsed.data.orgWide === true
|
||||
? null
|
||||
: parsed.data.orgWide === false
|
||||
? userId
|
||||
: existing.userId;
|
||||
|
||||
const [updated] = await db
|
||||
.update(launcherViews)
|
||||
.set({
|
||||
name: parsed.data.name ?? existing.name,
|
||||
config: parsed.data.config
|
||||
? JSON.stringify(parsed.data.config)
|
||||
: existing.config,
|
||||
userId: nextUserId,
|
||||
updatedAt: moment().toISOString()
|
||||
})
|
||||
.where(eq(launcherViews.viewId, viewId))
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
viewId: updated.viewId,
|
||||
orgId: updated.orgId,
|
||||
userId: updated.userId,
|
||||
name: updated.name,
|
||||
config: launcherViewConfigSchema.parse(
|
||||
JSON.parse(updated.config)
|
||||
),
|
||||
createdAt: updated.createdAt,
|
||||
updatedAt: updated.updatedAt,
|
||||
isOrgWide: updated.userId == null
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher view updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error updating launcher view:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,12 @@ import {
|
||||
userSiteResources,
|
||||
roleSiteResources,
|
||||
userOrgRoles,
|
||||
userOrgs
|
||||
userOrgs,
|
||||
labels,
|
||||
siteResourceLabels
|
||||
} from "@server/db";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { and, eq, inArray, asc, isNotNull, ne, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -19,13 +23,33 @@ import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
|
||||
const USER_RESOURCE_ALIASES_CACHE_TTL_SEC = 60;
|
||||
|
||||
const labelFilterQuerySchema = z
|
||||
.preprocess((val) => {
|
||||
if (val === undefined || val === null || val === "") {
|
||||
return undefined;
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return val;
|
||||
}
|
||||
if (typeof val === "string") {
|
||||
return val.split(",");
|
||||
}
|
||||
return undefined;
|
||||
}, z.array(z.string()))
|
||||
.optional()
|
||||
.catch([]);
|
||||
|
||||
function userResourceAliasesCacheKey(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
page: number,
|
||||
pageSize: number
|
||||
pageSize: number,
|
||||
includeLabels: boolean,
|
||||
labelFilter: string[]
|
||||
) {
|
||||
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}`;
|
||||
const labelsKey =
|
||||
labelFilter.length > 0 ? labelFilter.slice().sort().join(",") : "all";
|
||||
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}:${includeLabels ? "labels" : "plain"}:${labelsKey}`;
|
||||
}
|
||||
|
||||
const listUserResourceAliasesParamsSchema = z.strictObject({
|
||||
@@ -56,43 +80,35 @@ const listUserResourceAliasesQuerySchema = z.strictObject({
|
||||
type: "integer",
|
||||
default: 1,
|
||||
description: "Page number to retrieve"
|
||||
})
|
||||
}),
|
||||
includeLabels: z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.default("false")
|
||||
.transform((val) => val === "true")
|
||||
.openapi({
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description:
|
||||
"When true, include label names for each alias in the items field"
|
||||
}),
|
||||
labels: labelFilterQuerySchema.openapi({
|
||||
type: "array",
|
||||
description:
|
||||
"Filter by resource labels. A resource matches when it has any of the given labels (OR)."
|
||||
})
|
||||
});
|
||||
|
||||
export type UserResourceAliasItem = {
|
||||
alias: string;
|
||||
labels: string[];
|
||||
};
|
||||
|
||||
export type ListUserResourceAliasesResponse = PaginatedResponse<{
|
||||
aliases: string[];
|
||||
items?: UserResourceAliasItem[];
|
||||
}>;
|
||||
|
||||
// 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: {
|
||||
// 200: {
|
||||
// description: "Successful response",
|
||||
// content: {
|
||||
// "application/json": {
|
||||
// schema: z.object({
|
||||
// data: z.record(z.string(), z.any()).nullable(),
|
||||
// success: z.boolean(),
|
||||
// error: z.boolean(),
|
||||
// message: z.string(),
|
||||
// status: z.number()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
export async function listUserResourceAliases(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -110,7 +126,12 @@ export async function listUserResourceAliases(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { page, pageSize } = parsedQuery.data;
|
||||
const {
|
||||
page,
|
||||
pageSize,
|
||||
includeLabels,
|
||||
labels: labelFilter
|
||||
} = parsedQuery.data;
|
||||
|
||||
const parsedParams = listUserResourceAliasesParamsSchema.safeParse(
|
||||
req.params
|
||||
@@ -149,7 +170,9 @@ export async function listUserResourceAliases(
|
||||
orgId,
|
||||
userId,
|
||||
page,
|
||||
pageSize
|
||||
pageSize,
|
||||
includeLabels,
|
||||
labelFilter ?? []
|
||||
);
|
||||
const cachedData: ListUserResourceAliasesResponse | undefined =
|
||||
await cache.get(cacheKey);
|
||||
@@ -204,6 +227,7 @@ export async function listUserResourceAliases(
|
||||
if (accessibleSiteResourceIds.length === 0) {
|
||||
const data: ListUserResourceAliasesResponse = {
|
||||
aliases: [],
|
||||
...(includeLabels ? { items: [] } : {}),
|
||||
pagination: {
|
||||
total: 0,
|
||||
pageSize,
|
||||
@@ -224,18 +248,44 @@ export async function listUserResourceAliases(
|
||||
});
|
||||
}
|
||||
|
||||
const whereClause = and(
|
||||
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
|
||||
orgId,
|
||||
tierMatrix.labels
|
||||
);
|
||||
|
||||
const whereConditions = [
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true),
|
||||
or(eq(siteResources.mode, "host"), eq(siteResources.mode, "ssh")),
|
||||
isNotNull(siteResources.alias),
|
||||
ne(siteResources.alias, ""),
|
||||
inArray(siteResources.siteResourceId, accessibleSiteResourceIds)
|
||||
);
|
||||
];
|
||||
|
||||
if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
|
||||
whereConditions.push(
|
||||
inArray(
|
||||
siteResources.siteResourceId,
|
||||
db
|
||||
.select({ id: siteResourceLabels.siteResourceId })
|
||||
.from(siteResourceLabels)
|
||||
.innerJoin(
|
||||
labels,
|
||||
eq(labels.labelId, siteResourceLabels.labelId)
|
||||
)
|
||||
.where(inArray(labels.name, labelFilter))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const whereClause = and(...whereConditions);
|
||||
|
||||
const baseSelect = () =>
|
||||
db
|
||||
.select({ alias: siteResources.alias })
|
||||
.select({
|
||||
alias: siteResources.alias,
|
||||
siteResourceId: siteResources.siteResourceId
|
||||
})
|
||||
.from(siteResources)
|
||||
.where(whereClause);
|
||||
|
||||
@@ -251,8 +301,46 @@ export async function listUserResourceAliases(
|
||||
|
||||
const aliases = rows.map((r) => r.alias as string);
|
||||
|
||||
let items: UserResourceAliasItem[] | undefined;
|
||||
if (includeLabels) {
|
||||
const siteResourceIdList = rows.map((r) => r.siteResourceId);
|
||||
|
||||
let labelsForSiteResources: Array<{
|
||||
name: string;
|
||||
siteResourceId: number;
|
||||
}> = [];
|
||||
|
||||
if (isLabelFeatureEnabled && siteResourceIdList.length > 0) {
|
||||
labelsForSiteResources = await db
|
||||
.select({
|
||||
name: labels.name,
|
||||
siteResourceId: siteResourceLabels.siteResourceId
|
||||
})
|
||||
.from(labels)
|
||||
.innerJoin(
|
||||
siteResourceLabels,
|
||||
eq(siteResourceLabels.labelId, labels.labelId)
|
||||
)
|
||||
.where(
|
||||
inArray(
|
||||
siteResourceLabels.siteResourceId,
|
||||
siteResourceIdList
|
||||
)
|
||||
)
|
||||
.orderBy(asc(siteResourceLabels.siteResourceLabelId));
|
||||
}
|
||||
|
||||
items = rows.map((row) => ({
|
||||
alias: row.alias as string,
|
||||
labels: labelsForSiteResources
|
||||
.filter((l) => l.siteResourceId === row.siteResourceId)
|
||||
.map((l) => l.name)
|
||||
}));
|
||||
}
|
||||
|
||||
const data: ListUserResourceAliasesResponse = {
|
||||
aliases,
|
||||
...(items !== undefined ? { items } : {}),
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
pageSize,
|
||||
|
||||
Reference in New Issue
Block a user