Merge branch 'dev' into feat/command-bar

This commit is contained in:
Fred KISSIE
2026-07-08 00:33:28 +02:00
399 changed files with 32298 additions and 9787 deletions
+58
View File
@@ -0,0 +1,58 @@
import type { SiteResourceData } from "@app/lib/privateResourceForm";
import type { ListAllSiteResourcesByOrgResponse } from "@server/routers/siteResource";
import type { AxiosResponse } from "axios";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
export async function fetchSiteResourceByNiceId(
orgId: string,
niceId: string
): Promise<SiteResourceData | null> {
const res = await internal.get<
AxiosResponse<ListAllSiteResourcesByOrgResponse>
>(
`/org/${orgId}/site-resources?query=${encodeURIComponent(niceId)}&pageSize=50`,
await authCookieHeader()
);
const match = res.data.data.siteResources.find((r) => r.niceId === niceId);
if (!match) {
return null;
}
return {
id: match.siteResourceId,
name: match.name,
orgId,
sites: match.siteIds.map((siteId, idx) => ({
siteId,
siteName: match.siteNames[idx],
siteNiceId: match.siteNiceIds[idx],
online: match.siteOnlines[idx]
})),
mode: match.mode,
scheme: match.scheme,
ssl: match.ssl,
siteNames: match.siteNames,
siteAddresses: match.siteAddresses || null,
siteIds: match.siteIds,
destination: match.destination,
destinationPort: match.destinationPort ?? null,
alias: match.alias || null,
aliasAddress: match.aliasAddress || null,
siteNiceIds: match.siteNiceIds,
niceId: match.niceId,
tcpPortRangeString: match.tcpPortRangeString || null,
udpPortRangeString: match.udpPortRangeString || null,
disableIcmp: match.disableIcmp || false,
authDaemonMode: match.authDaemonMode ?? null,
authDaemonPort: match.authDaemonPort ?? null,
pamMode: match.pamMode ?? null,
subdomain: match.subdomain ?? null,
domainId: match.domainId ?? null,
fullDomain: match.fullDomain ?? null,
labels: match.labels ?? [],
enabled: match.enabled
};
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { cache } from "react";
export const getBrowserTargetForRequest = cache(async () => {
const headersList = await headers();
const host = headersList.get("host") || "";
const host = headersList.get("p-host") || headersList.get("host") || "";
const hostname = host.split(":")[0];
try {
+98
View File
@@ -0,0 +1,98 @@
export type LauncherActiveViewId = number | "default";
const LAST_VIEW_PREFIX = "pangolin:launcher:last-view:";
const GROUP_OPEN_PREFIX = "pangolin:launcher:group-open:";
function lastViewKey(orgId: string) {
return `${LAST_VIEW_PREFIX}${orgId}`;
}
function groupOpenKey(
orgId: string,
viewId: LauncherActiveViewId,
groupBy: "site" | "label" | "none"
) {
return `${GROUP_OPEN_PREFIX}${orgId}:${viewId}:${groupBy}`;
}
function readJson<T>(key: string, fallback: T): T {
if (typeof window === "undefined") {
return fallback;
}
try {
const raw = window.localStorage.getItem(key);
return raw ? (JSON.parse(raw) as T) : fallback;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return fallback;
}
}
function writeJson(key: string, value: unknown) {
if (typeof window === "undefined") {
return;
}
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn(`Error writing localStorage key "${key}":`, error);
}
}
export function readLauncherLastView(
orgId: string
): LauncherActiveViewId | null {
const value = readJson<LauncherActiveViewId | null>(
lastViewKey(orgId),
null
);
if (value === "default" || typeof value === "number") {
return value;
}
return null;
}
export function writeLauncherLastView(
orgId: string,
viewId: LauncherActiveViewId
) {
writeJson(lastViewKey(orgId), viewId);
}
export function readLauncherGroupOpenState(
orgId: string,
viewId: LauncherActiveViewId,
groupBy: "site" | "label" | "none"
): Record<string, boolean> {
return readJson<Record<string, boolean>>(
groupOpenKey(orgId, viewId, groupBy),
{}
);
}
export function readLauncherGroupOpen(
orgId: string,
viewId: LauncherActiveViewId,
groupBy: "site" | "label" | "none",
groupKey: string,
defaultOpen: boolean
): boolean {
const state = readLauncherGroupOpenState(orgId, viewId, groupBy);
return groupKey in state ? state[groupKey] : defaultOpen;
}
export function writeLauncherGroupOpen(
orgId: string,
viewId: LauncherActiveViewId,
groupBy: "site" | "label" | "none",
groupKey: string,
isOpen: boolean
) {
const state = readLauncherGroupOpenState(orgId, viewId, groupBy);
writeJson(groupOpenKey(orgId, viewId, groupBy), {
...state,
[groupKey]: isOpen
});
}
+123
View File
@@ -0,0 +1,123 @@
import {
formatSiteResourceDestinationDisplay,
type SiteResourceDestinationInput
} from "./formatSiteResourceAccess";
export {
formatSiteResourceDestinationDisplay,
resolveHttpHttpsDisplayPort,
type SiteResourceDestinationInput
} from "./formatSiteResourceAccess";
export type PublicResourceAccessInput = {
mode: string;
fullDomain: string | null;
ssl: boolean;
proxyPort: number | null;
wildcard: boolean;
};
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;
};
export function formatPublicResourceAccess(
resource: PublicResourceAccessInput
): LauncherAccessFields {
const browserModes = ["http", "ssh", "rdp", "vnc"];
if (!browserModes.includes(resource.mode)) {
const port = resource.proxyPort?.toString() ?? "";
return {
accessDisplay: port,
accessCopyValue: port,
accessUrl: null
};
}
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
};
}
export function isSafeUrlForLink(url: string): boolean {
try {
const parsed = new URL(url);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}
+19
View File
@@ -0,0 +1,19 @@
import type { LauncherResource } from "@server/routers/launcher/types";
export function getPrivateResourceSettingsHref(
orgId: string,
niceId: string
): string {
return `/${orgId}/settings/resources/private/${niceId}/general`;
}
export function getLauncherResourceAdminHref(
orgId: string,
resource: LauncherResource
): string {
if (resource.resourceType === "public") {
return `/${orgId}/settings/resources/public/${resource.niceId}/general`;
}
return getPrivateResourceSettingsHref(orgId, resource.niceId);
}
+93
View File
@@ -0,0 +1,93 @@
import type { GetResourceResponse } from "@server/routers/resource/getResource";
import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource";
export type PublicAuthState = "protected" | "not_protected" | "none";
const BROWSER_MODES = ["http", "ssh", "rdp", "vnc"];
export function derivePublicAuthState(
mode: string | null,
authInfo: Pick<
GetResourceAuthInfoResponse,
"password" | "pincode" | "sso" | "whitelist" | "headerAuth"
>
): PublicAuthState {
if (!BROWSER_MODES.includes(mode || "")) {
return "none";
}
if (
authInfo.password ||
authInfo.pincode ||
authInfo.sso ||
authInfo.whitelist ||
authInfo.headerAuth
) {
return "protected";
}
return "not_protected";
}
export function formatPublicResourceType(
resource: Pick<GetResourceResponse, "mode" | "ssl">
): string {
if (resource.mode === "http") {
return resource.ssl ? "HTTPS" : "HTTP";
}
const mode = (resource.mode || "").toLowerCase();
if (mode === "tcp") {
return "TCP";
}
if (mode === "udp") {
return "UDP";
}
return (resource.mode || "—").toUpperCase();
}
export type PortProtocolState = "all" | "blocked" | "custom";
function getPortProtocolState(
value: string | null | undefined
): PortProtocolState {
if (value === "*") {
return "all";
}
if (!value || value.trim() === "") {
return "blocked";
}
return "custom";
}
export type PortRestrictionDisplay = {
hasNonDefaultPorts: boolean;
tcp: { state: PortProtocolState; ports: string | null };
udp: { state: PortProtocolState; ports: string | null };
};
export function formatPortRestrictionDisplay(
resource: Pick<
GetSiteResourceResponse,
"tcpPortRangeString" | "udpPortRangeString"
>
): PortRestrictionDisplay {
const tcpState = getPortProtocolState(resource.tcpPortRangeString);
const udpState = getPortProtocolState(resource.udpPortRangeString);
return {
hasNonDefaultPorts: tcpState !== "all" || udpState !== "all",
tcp: {
state: tcpState,
ports: tcpState === "custom" ? resource.tcpPortRangeString : null
},
udp: {
state: udpState,
ports: udpState === "custom" ? resource.udpPortRangeString : null
}
};
}
+114
View File
@@ -0,0 +1,114 @@
import type {
LauncherScaleInfo,
LauncherViewConfig
} from "@server/routers/launcher/types";
export function hasActiveLauncherFilters(config: LauncherViewConfig): boolean {
return (
config.query.trim().length > 0 ||
config.siteIds.length > 0 ||
config.labelIds.length > 0
);
}
export function getEffectiveGroupBy(
scale: LauncherScaleInfo,
config: LauncherViewConfig
): LauncherViewConfig["groupBy"] {
if (scale.mode !== "compact") {
return config.groupBy;
}
if (
config.groupBy === "site" &&
config.siteIds.length > 0 &&
scale.capabilities.allowSiteGrouping
) {
return "site";
}
if (
config.groupBy === "label" &&
config.labelIds.length > 0 &&
scale.capabilities.allowLabelGrouping
) {
return "label";
}
return "none";
}
export function getEffectiveLauncherConfig(
scale: LauncherScaleInfo,
config: LauncherViewConfig
): LauncherViewConfig {
const groupBy = getEffectiveGroupBy(scale, config);
if (groupBy === config.groupBy) {
return config;
}
return { ...config, groupBy };
}
export function shouldFetchLauncherGroups(
scale: LauncherScaleInfo,
config: LauncherViewConfig
): boolean {
const groupBy = getEffectiveGroupBy(scale, config);
if (groupBy === "none") {
return false;
}
if (scale.mode === "full") {
return true;
}
return (
(scale.capabilities.allowSiteGrouping &&
groupBy === "site" &&
config.siteIds.length > 0) ||
(scale.capabilities.allowLabelGrouping &&
groupBy === "label" &&
config.labelIds.length > 0)
);
}
export function shouldShowLauncherGroupList(
scale: LauncherScaleInfo,
config: LauncherViewConfig
): boolean {
return shouldFetchLauncherGroups(scale, config);
}
export function shouldShowSearchFirstGate(
scale: LauncherScaleInfo,
config: LauncherViewConfig
): boolean {
return (
scale.mode === "compact" &&
scale.capabilities.requireSearchOrFilter &&
!hasActiveLauncherFilters(config)
);
}
export function shouldShowFlatResourceList(
scale: LauncherScaleInfo,
config: LauncherViewConfig
): boolean {
if (shouldShowSearchFirstGate(scale, config)) {
return false;
}
const groupBy = getEffectiveGroupBy(scale, config);
if (groupBy === "none") {
return true;
}
if (scale.mode === "full") {
return false;
}
return !shouldShowLauncherGroupList(scale, config);
}
+43
View File
@@ -0,0 +1,43 @@
import type { LauncherListQuery } from "@server/routers/launcher/types";
export type LauncherQueryFilters = {
query?: string;
groupBy?: LauncherListQuery["groupBy"];
groupKey?: string;
siteIds?: number[];
labelIds?: number[];
sort_by?: LauncherListQuery["sort_by"];
order?: LauncherListQuery["order"];
pageSize?: number;
};
export function buildLauncherSearchParams(
filters: LauncherQueryFilters,
page: number
) {
const sp = new URLSearchParams();
sp.set("page", String(page));
sp.set("pageSize", String(filters.pageSize ?? 20));
if (filters.query) {
sp.set("query", filters.query);
}
if (filters.groupBy) {
sp.set("groupBy", filters.groupBy);
}
if (filters.groupKey) {
sp.set("groupKey", filters.groupKey);
}
if (filters.siteIds?.length) {
sp.set("siteIds", filters.siteIds.join(","));
}
if (filters.labelIds?.length) {
sp.set("labelIds", filters.labelIds.join(","));
}
if (filters.sort_by) {
sp.set("sort_by", filters.sort_by);
}
if (filters.order) {
sp.set("order", filters.order);
}
return sp;
}
+129
View File
@@ -0,0 +1,129 @@
import { internal } from "@app/lib/api";
import type { LauncherActiveViewId } from "@app/lib/launcherLocalStorage";
import { shouldFetchLauncherGroups } from "@app/lib/launcherScale";
import { resolveLauncherStateFromUrl } from "@app/lib/launcherUrlState";
import { buildLauncherSearchParams } from "@app/lib/launcherSearchParams";
import type {
LauncherGroup,
LauncherScaleInfo,
LauncherDefaultViewOverrides,
LauncherViewConfig,
LauncherViewRecord,
ListLauncherGroupsResponse,
ListLauncherScaleResponse,
ListLauncherViewsResponse
} from "@server/routers/launcher/types";
import { AxiosResponse } from "axios";
export type LauncherPageData = {
views: LauncherViewRecord[];
defaultViewOverrides: LauncherDefaultViewOverrides;
activeViewId: LauncherActiveViewId;
config: LauncherViewConfig;
savedConfig: LauncherViewConfig;
scale: LauncherScaleInfo;
groups: LauncherGroup[];
groupsPagination: {
total: number;
page: number;
pageSize: number;
};
};
export async function fetchLauncherPageData(
orgId: string,
searchParams: URLSearchParams,
cookieHeader: Awaited<
ReturnType<typeof import("@app/lib/api/cookies").authCookieHeader>
>
): Promise<LauncherPageData> {
let views: LauncherViewRecord[] = [];
let defaultViewOverrides: LauncherDefaultViewOverrides = {
personal: null,
orgWide: null
};
try {
const viewsRes = await internal.get<
AxiosResponse<ListLauncherViewsResponse>
>(`/org/${orgId}/launcher/views`, cookieHeader);
views = viewsRes.data.data.views;
defaultViewOverrides = viewsRes.data.data.defaultViewOverrides;
} catch (e) {}
const { activeViewId, config, savedConfig } = resolveLauncherStateFromUrl(
searchParams,
views,
null,
defaultViewOverrides
);
const scaleFilters = {
query: config.query,
groupBy: config.groupBy,
siteIds: config.siteIds,
labelIds: config.labelIds,
sort_by: config.sortBy,
order: config.order
};
let scale: LauncherScaleInfo = {
mode: "full",
resourceCount: 0,
siteGroupCount: 0,
labelGroupCount: 0,
capabilities: {
allowSiteGrouping: true,
allowLabelGrouping: true,
requireSearchOrFilter: false
}
};
try {
const sp = buildLauncherSearchParams(scaleFilters, 1);
sp.delete("page");
sp.delete("pageSize");
const scaleRes = await internal.get<
AxiosResponse<ListLauncherScaleResponse>
>(`/org/${orgId}/launcher/scale?${sp.toString()}`, cookieHeader);
scale = scaleRes.data.data.scale;
} catch (e) {}
const groupFilters = {
query: config.query,
groupBy: config.groupBy,
siteIds: config.siteIds,
labelIds: config.labelIds,
sort_by: config.sortBy,
order: config.order,
pageSize: 20
};
let groups: LauncherGroup[] = [];
let groupsPagination: LauncherPageData["groupsPagination"] = {
total: 0,
page: 1,
pageSize: 20
};
if (shouldFetchLauncherGroups(scale, config)) {
try {
const sp = buildLauncherSearchParams(groupFilters, 1);
const groupsRes = await internal.get<
AxiosResponse<ListLauncherGroupsResponse>
>(`/org/${orgId}/launcher/groups?${sp.toString()}`, cookieHeader);
groups = groupsRes.data.data.groups;
groupsPagination = groupsRes.data.data.pagination;
} catch (e) {}
}
return {
views,
defaultViewOverrides,
activeViewId,
config,
savedConfig,
scale,
groups,
groupsPagination
};
}
+292
View File
@@ -0,0 +1,292 @@
import type { LauncherActiveViewId } from "@app/lib/launcherLocalStorage";
import {
defaultLauncherViewConfig,
getEffectiveDefaultLauncherConfig,
parseIdListParam,
type LauncherDefaultViewOverrides,
type LauncherViewConfig,
type LauncherViewRecord
} from "@server/routers/launcher/types";
import { z } from "zod";
const launcherUrlBooleanSchema = z
.enum(["0", "1"])
.transform((value) => value === "1");
export type LauncherUrlConfigOverrides = Partial<
Pick<
LauncherViewConfig,
| "groupBy"
| "layout"
| "order"
| "showLabels"
| "siteIds"
| "labelIds"
| "query"
>
>;
export type ParsedLauncherUrlState = {
viewId: LauncherActiveViewId | null;
configOverrides: LauncherUrlConfigOverrides;
hasAnyLauncherParams: boolean;
};
export type ResolvedLauncherState = {
activeViewId: LauncherActiveViewId;
config: LauncherViewConfig;
savedConfig: LauncherViewConfig;
};
const LAUNCHER_CONFIG_PARAM_KEYS = [
"query",
"groupBy",
"layout",
"order",
"showLabels",
"siteIds",
"labelIds"
] as const;
const LAUNCHER_URL_PARAM_KEYS = [
"view",
...LAUNCHER_CONFIG_PARAM_KEYS
] as const;
export function hasLauncherConfigParams(searchParams: URLSearchParams) {
return LAUNCHER_CONFIG_PARAM_KEYS.some((key) => searchParams.has(key));
}
export function isLauncherConfigEqual(
a: LauncherViewConfig,
b: LauncherViewConfig
) {
return JSON.stringify(a) === JSON.stringify(b);
}
export function getLauncherUrlBaseConfig(
viewId: LauncherActiveViewId,
views: LauncherViewRecord[],
defaultViewOverrides?: LauncherDefaultViewOverrides
): LauncherViewConfig {
if (viewId === "default") {
return defaultViewOverrides
? getEffectiveDefaultLauncherConfig(defaultViewOverrides)
: defaultLauncherViewConfig;
}
const savedView = views.find((view) => view.viewId === viewId);
return savedView?.config ?? defaultLauncherViewConfig;
}
export function resolveLauncherConfig(
baseConfig: LauncherViewConfig,
overrides: LauncherUrlConfigOverrides
): LauncherViewConfig {
return {
...baseConfig,
...overrides,
sortBy: "name"
};
}
function parseViewParam(value: string | null): LauncherActiveViewId | null {
if (value === null) {
return null;
}
if (value === "default") {
return "default";
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) {
return "default";
}
return parsed;
}
function parseConfigOverrides(
searchParams: URLSearchParams
): LauncherUrlConfigOverrides {
const overrides: LauncherUrlConfigOverrides = {};
const query = searchParams.get("query");
if (query !== null) {
overrides.query = query;
}
const groupBy = searchParams.get("groupBy");
if (groupBy === "site" || groupBy === "label" || groupBy === "none") {
overrides.groupBy = groupBy;
}
const layout = searchParams.get("layout");
if (layout === "grid" || layout === "list") {
overrides.layout = layout;
}
const order = searchParams.get("order");
if (order === "asc" || order === "desc") {
overrides.order = order;
}
const showLabels = searchParams.get("showLabels");
if (showLabels !== null) {
const parsed = launcherUrlBooleanSchema.safeParse(showLabels);
if (parsed.success) {
overrides.showLabels = parsed.data;
}
}
const siteIds = searchParams.get("siteIds");
if (siteIds !== null) {
overrides.siteIds = parseIdListParam(siteIds);
}
const labelIds = searchParams.get("labelIds");
if (labelIds !== null) {
overrides.labelIds = parseIdListParam(labelIds);
}
return overrides;
}
export function parseLauncherUrlState(
searchParams: URLSearchParams
): ParsedLauncherUrlState {
const hasAnyLauncherParams = LAUNCHER_URL_PARAM_KEYS.some((key) =>
searchParams.has(key)
);
return {
viewId: parseViewParam(searchParams.get("view")),
configOverrides: parseConfigOverrides(searchParams),
hasAnyLauncherParams
};
}
function isValidActiveViewId(
viewId: LauncherActiveViewId,
views: LauncherViewRecord[]
) {
return viewId === "default" || views.some((view) => view.viewId === viewId);
}
export function resolveLauncherStateFromUrl(
searchParams: URLSearchParams,
views: LauncherViewRecord[],
fallbackViewId: LauncherActiveViewId | null,
defaultViewOverrides?: LauncherDefaultViewOverrides
): ResolvedLauncherState {
const parsed = parseLauncherUrlState(searchParams);
let activeViewId: LauncherActiveViewId = "default";
if (parsed.viewId !== null) {
activeViewId = isValidActiveViewId(parsed.viewId, views)
? parsed.viewId
: "default";
} else if (!parsed.hasAnyLauncherParams && fallbackViewId !== null) {
activeViewId = isValidActiveViewId(fallbackViewId, views)
? fallbackViewId
: "default";
}
const savedConfig = getLauncherUrlBaseConfig(
activeViewId,
views,
defaultViewOverrides
);
let config: LauncherViewConfig;
if (hasLauncherConfigParams(searchParams)) {
config = resolveLauncherConfig(
getEffectiveDefaultLauncherConfig(
defaultViewOverrides ?? { personal: null, orgWide: null }
),
parsed.configOverrides
);
} else if (activeViewId !== "default") {
config = savedConfig;
} else {
config = getEffectiveDefaultLauncherConfig(
defaultViewOverrides ?? { personal: null, orgWide: null }
);
}
return {
activeViewId,
config,
savedConfig
};
}
function idListsEqual(a: number[], b: number[]) {
if (a.length !== b.length) {
return false;
}
return a.every((value, index) => value === b[index]);
}
export function serializeLauncherUrlState({
viewId,
config
}: {
viewId: LauncherActiveViewId;
config: LauncherViewConfig;
}): URLSearchParams {
const baseConfig = defaultLauncherViewConfig;
const params = new URLSearchParams();
if (viewId !== "default") {
params.set("view", String(viewId));
}
if (config.query !== baseConfig.query && config.query) {
params.set("query", config.query);
} else if (config.query !== baseConfig.query && !config.query) {
params.set("query", "");
}
if (config.groupBy !== baseConfig.groupBy) {
params.set("groupBy", config.groupBy);
}
if (config.layout !== baseConfig.layout) {
params.set("layout", config.layout);
}
if (config.order !== baseConfig.order) {
params.set("order", config.order);
}
if (config.showLabels !== baseConfig.showLabels) {
params.set("showLabels", config.showLabels ? "1" : "0");
}
if (!idListsEqual(config.siteIds, baseConfig.siteIds)) {
if (config.siteIds.length > 0) {
params.set("siteIds", config.siteIds.join(","));
} else {
params.set("siteIds", "");
}
}
if (!idListsEqual(config.labelIds, baseConfig.labelIds)) {
if (config.labelIds.length > 0) {
params.set("labelIds", config.labelIds.join(","));
} else {
params.set("labelIds", "");
}
}
return params;
}
export function buildLauncherPath(orgId: string, params: URLSearchParams) {
const query = params.toString();
return query ? `/${orgId}?${query}` : `/${orgId}`;
}
+658
View File
@@ -0,0 +1,658 @@
import { z } from "zod";
import type { InternalResourceRow } from "@app/components/PrivateResourcesTable";
export type PrivateResourceMode = "host" | "cidr" | "http" | "ssh";
export type SiteResourceData = InternalResourceRow & {
enabled: boolean;
};
export type PortMode = "all" | "blocked" | "custom";
const tagSchema = z.object({ id: z.string(), text: z.string() });
export type PrivateResourceAccessTag = z.infer<typeof tagSchema>;
export type PrivateResourceClient = {
clientId: number;
name: string;
};
export type PrivateResourceFormValues = {
name: string;
siteIds: number[];
mode: PrivateResourceMode;
destination: string | null;
alias?: string | null;
niceId?: string;
enabled?: boolean;
tcpPortRangeString?: string | null;
udpPortRangeString?: string | null;
disableIcmp?: boolean;
authDaemonMode?: "site" | "remote" | "native" | null;
authDaemonPort?: number | null;
pamMode?: "passthrough" | "push" | null;
destinationPort?: number | null;
scheme?: "http" | "https";
ssl?: boolean;
httpConfigSubdomain?: string | null;
httpConfigDomainId?: string | null;
httpConfigFullDomain?: string | null;
roles?: PrivateResourceAccessTag[];
users?: PrivateResourceAccessTag[];
clients?: PrivateResourceClient[];
};
export type SiteResourceAccess = {
roleIds: number[];
userIds: string[];
clientIds: number[];
};
type TranslateFn = (key: string) => string;
export const isValidPortRangeString = (
val: string | undefined | null
): boolean => {
if (!val || val.trim() === "" || val.trim() === "*") return true;
const parts = val.split(",").map((p) => p.trim());
for (const part of parts) {
if (part === "") return false;
if (part.includes("-")) {
const [start, end] = part.split("-").map((p) => p.trim());
if (!start || !end) return false;
const startPort = parseInt(start, 10);
const endPort = parseInt(end, 10);
if (isNaN(startPort) || isNaN(endPort)) return false;
if (
startPort < 1 ||
startPort > 65535 ||
endPort < 1 ||
endPort > 65535
)
return false;
if (startPort > endPort) return false;
} else {
const port = parseInt(part, 10);
if (isNaN(port) || port < 1 || port > 65535) return false;
}
}
return true;
};
const getPortRangeValidationMessage = (t: TranslateFn) =>
t("editInternalResourceDialogPortRangeValidationError");
export const createPortRangeStringSchema = (t: TranslateFn) =>
z
.string()
.optional()
.nullable()
.refine((val) => isValidPortRangeString(val), {
message: getPortRangeValidationMessage(t)
});
export const getPortModeFromString = (
val: string | undefined | null
): PortMode => {
if (val === "*") return "all";
if (!val || val.trim() === "") return "blocked";
return "custom";
};
export const getPortStringFromMode = (
mode: PortMode,
customValue: string
): string | undefined => {
if (mode === "all") return "*";
if (mode === "blocked") return "";
return customValue;
};
export const isHostname = (destination: string | null): boolean =>
!!destination && /[a-zA-Z]/.test(destination);
export const cleanForFQDN = (name: string): string =>
name
.toLowerCase()
.replace(/[^a-z0-9.-]/g, "-")
.replace(/[-]+/g, "-")
.replace(/^-|-$/g, "")
.replace(/^\.|\.$/g, "");
export function applyAliasAutoGeneration(
values: PrivateResourceFormValues
): PrivateResourceFormValues {
const data = { ...values };
if (
(data.mode === "host" ||
data.mode === "http" ||
(data.mode === "ssh" && data.authDaemonMode !== "native")) &&
isHostname(data.destination)
) {
const currentAlias = data.alias?.trim() || "";
if (!currentAlias) {
let aliasValue = data.destination!;
if (data.destination?.toLowerCase() === "localhost") {
aliasValue = `${cleanForFQDN(data.name)}.internal`;
}
data.alias = aliasValue;
}
}
return data;
}
export function accessTagsToIds(access: {
roles?: PrivateResourceAccessTag[];
users?: PrivateResourceAccessTag[];
clients?: PrivateResourceClient[];
}): SiteResourceAccess {
return {
roleIds: (access.roles ?? []).map((r) => parseInt(r.id)),
userIds: (access.users ?? []).map((u) => u.id),
clientIds: (access.clients ?? []).map((c) => c.clientId)
};
}
export function buildCreateSiteResourcePayload(
values: PrivateResourceFormValues
) {
const data = applyAliasAutoGeneration(values);
const isNativeSsh = data.mode === "ssh" && data.authDaemonMode === "native";
return {
name: data.name,
siteIds: data.siteIds,
mode: data.mode,
destination: isNativeSsh ? undefined : (data.destination ?? undefined),
enabled: true,
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
destinationPort: data.destinationPort ?? undefined,
domainId: data.httpConfigDomainId
? data.httpConfigDomainId
: undefined,
subdomain: data.httpConfigSubdomain
? data.httpConfigSubdomain
: undefined
}),
...(data.mode === "host" && {
alias:
data.alias &&
typeof data.alias === "string" &&
data.alias.trim()
? data.alias
: undefined,
...(data.authDaemonMode != null && {
authDaemonMode: data.authDaemonMode
}),
...(data.authDaemonMode === "remote" &&
data.authDaemonPort != null && {
authDaemonPort: data.authDaemonPort
})
}),
...(data.mode === "ssh" && {
alias:
data.alias &&
typeof data.alias === "string" &&
data.alias.trim()
? data.alias
: undefined,
...(!isNativeSsh && {
destinationPort: data.destinationPort ?? undefined
}),
pamMode: data.pamMode ?? undefined,
...(data.authDaemonMode != null && {
authDaemonMode: data.authDaemonMode
}),
...(data.authDaemonMode === "remote" &&
data.authDaemonPort != null && {
authDaemonPort: data.authDaemonPort
})
}),
...((data.mode === "host" || data.mode === "cidr") && {
tcpPortRangeString: data.tcpPortRangeString,
udpPortRangeString: data.udpPortRangeString,
disableIcmp: data.disableIcmp ?? false
}),
roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [],
userIds: data.users ? data.users.map((u) => u.id) : [],
clientIds: data.clients ? data.clients.map((c) => c.clientId) : []
};
}
export function buildUpdateSiteResourcePayload(
values: PrivateResourceFormValues,
access: SiteResourceAccess
) {
const data = applyAliasAutoGeneration(values);
const isNativeSsh = data.mode === "ssh" && data.authDaemonMode === "native";
return {
name: data.name,
siteIds: data.siteIds,
mode: data.mode,
niceId: data.niceId,
enabled: data.enabled,
...(isNativeSsh
? { destination: null, destinationPort: null }
: { destination: data.destination ?? undefined }),
...(data.mode === "http" && {
scheme: data.scheme,
ssl: data.ssl ?? false,
destinationPort: data.destinationPort ?? null,
domainId: data.httpConfigDomainId
? data.httpConfigDomainId
: undefined,
subdomain: data.httpConfigSubdomain
? data.httpConfigSubdomain
: undefined
}),
...(data.mode === "host" && {
alias:
data.alias &&
typeof data.alias === "string" &&
data.alias.trim()
? data.alias
: null,
...(data.authDaemonMode != null && {
authDaemonMode: data.authDaemonMode
}),
...(data.authDaemonMode === "remote" && {
authDaemonPort: data.authDaemonPort || null
})
}),
...(data.mode === "ssh" && {
alias:
data.alias &&
typeof data.alias === "string" &&
data.alias.trim()
? data.alias
: null,
...(!isNativeSsh && {
destinationPort: data.destinationPort ?? null
}),
pamMode: data.pamMode ?? undefined,
...(data.authDaemonMode != null && {
authDaemonMode: data.authDaemonMode
}),
...(data.authDaemonMode === "remote" && {
authDaemonPort: data.authDaemonPort || null
})
}),
...((data.mode === "host" || data.mode === "cidr") && {
tcpPortRangeString: data.tcpPortRangeString,
udpPortRangeString: data.udpPortRangeString,
disableIcmp: data.disableIcmp ?? false
}),
roleIds: access.roleIds,
userIds: access.userIds,
clientIds: access.clientIds
};
}
export function inferSshPamMode(
authDaemonMode?: string | null,
pamMode?: "passthrough" | "push" | null
): "passthrough" | "push" {
if (pamMode === "passthrough" || pamMode === "push") {
return pamMode;
}
return authDaemonMode === "remote" ? "push" : "passthrough";
}
export function siteResourceToFormValues(
resource: SiteResourceData
): PrivateResourceFormValues {
return {
name: resource.name,
siteIds: resource.siteIds,
mode: resource.mode ?? "host",
destination: resource.destination ?? "",
alias: resource.alias ?? null,
destinationPort: resource.destinationPort ?? null,
scheme: resource.scheme ?? "http",
ssl: resource.ssl ?? false,
httpConfigSubdomain: resource.subdomain ?? null,
httpConfigDomainId: resource.domainId ?? null,
httpConfigFullDomain: resource.fullDomain ?? null,
tcpPortRangeString: resource.tcpPortRangeString ?? "*",
udpPortRangeString: resource.udpPortRangeString ?? "*",
disableIcmp: resource.disableIcmp ?? false,
authDaemonMode:
resource.authDaemonMode === "native"
? "native"
: (resource.authDaemonMode ?? "site"),
authDaemonPort: resource.authDaemonPort ?? null,
pamMode: inferSshPamMode(resource.authDaemonMode, resource.pamMode),
niceId: resource.niceId,
enabled: resource.enabled
};
}
export function createGeneralFormSchema(t: TranslateFn) {
return z.object({
name: z
.string()
.min(1, t("editInternalResourceDialogNameRequired"))
.max(255, t("editInternalResourceDialogNameMaxLength")),
niceId: z
.string()
.min(1)
.max(255)
.regex(/^[a-zA-Z0-9-]+$/)
});
}
export function createAccessFormSchema() {
return z.object({
roles: z.array(tagSchema).optional(),
users: z.array(tagSchema).optional(),
clients: z
.array(
z.object({
clientId: z.number(),
name: z.string()
})
)
.optional()
});
}
export function createCreateFormSchema(t: TranslateFn) {
const destinationRequired = t(
"createInternalResourceDialogDestinationRequired"
);
return z
.object({
name: z
.string()
.min(1, t("createInternalResourceDialogNameRequired"))
.max(255, t("createInternalResourceDialogNameMaxLength")),
siteIds: z
.array(z.number().int().positive())
.min(1, t("createInternalResourceDialogPleaseSelectSite")),
mode: z.enum(["host", "cidr", "http", "ssh"]),
destination: z.string().nullish(),
alias: z.string().nullish(),
destinationPort: z
.number()
.int()
.min(1)
.max(65535)
.optional()
.nullable(),
scheme: z.enum(["http", "https"]).optional(),
ssl: z.boolean().optional(),
httpConfigSubdomain: z.string().nullish(),
httpConfigDomainId: z.string().nullish(),
httpConfigFullDomain: z.string().nullish(),
authDaemonMode: z
.enum(["site", "remote", "native"])
.optional()
.nullable(),
standardDaemonLocation: z
.enum(["site", "remote"])
.optional()
.nullable(),
authDaemonPort: z.number().int().positive().optional().nullable(),
pamMode: z.enum(["passthrough", "push"]).optional().nullable(),
tcpPortRangeString: createPortRangeStringSchema(t),
udpPortRangeString: createPortRangeStringSchema(t),
disableIcmp: z.boolean().optional()
})
.superRefine((data, ctx) => {
const isNativeSsh =
data.mode === "ssh" && data.authDaemonMode === "native";
const trimmedDestination = data.destination?.trim();
if (
data.mode !== "ssh" &&
(!trimmedDestination || trimmedDestination.length < 1)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: destinationRequired,
path: ["destination"]
});
}
if (data.mode === "ssh" && !isNativeSsh) {
if (!trimmedDestination || trimmedDestination.length < 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: destinationRequired,
path: ["destination"]
});
}
if (
data.destinationPort == null ||
!Number.isFinite(data.destinationPort) ||
data.destinationPort < 1
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceHttpPortRequired"),
path: ["destinationPort"]
});
}
}
if (data.mode === "http") {
if (!data.scheme) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceDownstreamSchemeRequired"),
path: ["scheme"]
});
}
if (
data.destinationPort == null ||
!Number.isFinite(data.destinationPort) ||
data.destinationPort < 1
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceHttpPortRequired"),
path: ["destinationPort"]
});
}
if (!data.httpConfigDomainId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("domainRequired"),
path: ["httpConfigDomainId"]
});
}
}
});
}
function destinationRefine(
data: {
mode: PrivateResourceMode;
destination?: string | null;
authDaemonMode?: string | null;
destinationPort?: number | null;
scheme?: string;
httpConfigDomainId?: string | null;
},
ctx: z.RefinementCtx,
t: TranslateFn,
destinationRequired?: string
) {
const isNativeSsh = data.mode === "ssh" && data.authDaemonMode === "native";
const trimmedDestination = data.destination?.trim();
if (
!isNativeSsh &&
(!trimmedDestination || trimmedDestination.length < 1)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: destinationRequired ?? "Destination is required",
path: ["destination"]
});
}
if (data.mode === "ssh" && !isNativeSsh) {
if (
data.destinationPort == null ||
!Number.isFinite(data.destinationPort) ||
data.destinationPort < 1
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceHttpPortRequired"),
path: ["destinationPort"]
});
}
}
if (data.mode === "http") {
if (!data.scheme) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceDownstreamSchemeRequired"),
path: ["scheme"]
});
}
if (
data.destinationPort == null ||
!Number.isFinite(data.destinationPort) ||
data.destinationPort < 1
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("internalResourceHttpPortRequired"),
path: ["destinationPort"]
});
}
if (!data.httpConfigDomainId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("domainRequired"),
path: ["httpConfigDomainId"]
});
}
}
}
export function createHostFormSchema(t: TranslateFn) {
return z
.object({
siteIds: z.array(z.number().int().positive()).min(1),
mode: z.literal("host"),
destination: z.string().nullish(),
alias: z.string().nullish(),
tcpPortRangeString: createPortRangeStringSchema(t),
udpPortRangeString: createPortRangeStringSchema(t),
disableIcmp: z.boolean().optional(),
authDaemonMode: z
.enum(["site", "remote", "native"])
.optional()
.nullable(),
authDaemonPort: z.number().int().positive().optional().nullable()
})
.superRefine((data, ctx) => destinationRefine(data, ctx, t));
}
export function createCidrFormSchema(t: TranslateFn) {
return z
.object({
siteIds: z.array(z.number().int().positive()).min(1),
mode: z.literal("cidr"),
destination: z.string().nullish(),
tcpPortRangeString: createPortRangeStringSchema(t),
udpPortRangeString: createPortRangeStringSchema(t),
disableIcmp: z.boolean().optional()
})
.superRefine((data, ctx) => destinationRefine(data, ctx, t));
}
export function createHttpFormSchema(t: TranslateFn) {
return z
.object({
siteIds: z.array(z.number().int().positive()).min(1),
mode: z.literal("http"),
destination: z.string().nullish(),
destinationPort: z
.number()
.int()
.min(1)
.max(65535)
.optional()
.nullable(),
scheme: z.enum(["http", "https"]).optional(),
ssl: z.boolean().optional(),
httpConfigSubdomain: z.string().nullish(),
httpConfigDomainId: z.string().nullish(),
httpConfigFullDomain: z.string().nullish()
})
.superRefine((data, ctx) => destinationRefine(data, ctx, t));
}
export function createSshFormSchema(
t: TranslateFn,
options?: { isNative?: boolean }
) {
const isNative = options?.isNative ?? false;
return z
.object({
siteIds: z.array(z.number().int().positive()).min(1),
mode: z.literal("ssh"),
destination: z.string().nullish(),
alias: z.string().nullish(),
destinationPort: z
.number()
.int()
.min(1)
.max(65535)
.optional()
.nullable(),
pamMode: z.enum(["passthrough", "push"]),
standardDaemonLocation: z.enum(["site", "remote"]),
authDaemonPort: z.string()
})
.superRefine((data, ctx) => {
destinationRefine(
{
...data,
authDaemonMode: isNative
? "native"
: data.standardDaemonLocation
},
ctx,
t
);
const showDaemonPort =
!isNative &&
data.pamMode === "push" &&
data.standardDaemonLocation === "remote";
if (showDaemonPort) {
const port = Number(data.authDaemonPort);
if (
!data.authDaemonPort.trim() ||
!Number.isInteger(port) ||
port < 1 ||
port > 65535
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("healthCheckPortInvalid"),
path: ["authDaemonPort"]
});
}
}
});
}
export function mergeFormValuesWithResource(
resource: SiteResourceData,
partial: Partial<PrivateResourceFormValues>
): PrivateResourceFormValues {
return {
...siteResourceToFormValues(resource),
...partial
};
}
+224
View File
@@ -50,6 +50,25 @@ import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
import { StatusHistoryResponse } from "@server/lib/statusHistory";
import type { ListResourcePoliciesResponse } from "@server/routers/resource/types";
import type { GetResourcePolicyResponse } from "@server/routers/policy";
import type {
ListLauncherGroupsResponse,
ListLauncherLabelsResponse,
ListLauncherResourcesResponse,
ListLauncherScaleResponse,
ListLauncherSitesResponse,
ListLauncherViewsResponse,
LauncherListQuery,
LauncherResource,
LauncherViewConfig
} from "@server/routers/launcher/types";
import type { GetResourceResponse } from "@server/routers/resource/getResource";
import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource";
import type { LauncherQueryFilters } from "@app/lib/launcherSearchParams";
import { buildLauncherSearchParams } from "@app/lib/launcherSearchParams";
export type { LauncherQueryFilters } from "@app/lib/launcherSearchParams";
export { buildLauncherSearchParams } from "@app/lib/launcherSearchParams";
export type ProductUpdate = {
link: string | null;
@@ -67,6 +86,34 @@ export type LatestVersionResponse = {
latestVersion: string;
releaseNotes: string;
};
newt: {
latestVersion: string;
releaseNotes: string;
};
cli: {
latestVersion: string;
releaseNotes: string;
};
"panglin-node": {
latestVersion: string;
releaseNotes: string;
};
windows: {
latestVersion: string;
releaseNotes: string;
};
android: {
latestVersion: string;
releaseNotes: string;
};
mac: {
latestVersion: string;
releaseNotes: string;
};
ios: {
latestVersion: string;
releaseNotes: string;
};
};
export const productUpdatesQueries = {
@@ -1212,3 +1259,180 @@ export const domainQueries = {
refetchInterval: durationToMs(10, "seconds")
})
};
export const launcherQueries = {
views: (orgId: string) =>
queryOptions({
queryKey: ["ORG", orgId, "LAUNCHER", "VIEWS"] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListLauncherViewsResponse>
>(`/org/${orgId}/launcher/views`, { signal });
return res.data.data;
}
}),
sites: ({
orgId,
query,
perPage = 20
}: {
orgId: string;
query?: string;
perPage?: number;
}) =>
queryOptions({
queryKey: [
"ORG",
orgId,
"LAUNCHER",
"SITES",
{ query, perPage }
] as const,
queryFn: async ({ signal, meta }) => {
const sp = new URLSearchParams({
pageSize: perPage.toString()
});
if (query?.trim()) {
sp.set("query", query);
}
const res = await meta!.api.get<
AxiosResponse<ListLauncherSitesResponse>
>(`/org/${orgId}/launcher/sites?${sp.toString()}`, { signal });
return res.data.data.sites;
}
}),
labels: ({
orgId,
query,
perPage = 20
}: {
orgId: string;
query?: string;
perPage?: number;
}) =>
queryOptions({
queryKey: [
"ORG",
orgId,
"LAUNCHER",
"LABELS",
{ query, perPage }
] as const,
queryFn: async ({ signal, meta }) => {
const sp = new URLSearchParams({
pageSize: perPage.toString()
});
if (query?.trim()) {
sp.set("query", query);
}
const res = await meta!.api.get<
AxiosResponse<ListLauncherLabelsResponse>
>(`/org/${orgId}/launcher/labels?${sp.toString()}`, {
signal
});
return res.data.data.labels;
}
}),
groups: (orgId: string, filters: LauncherQueryFilters) =>
infiniteQueryOptions({
queryKey: ["ORG", orgId, "LAUNCHER", "GROUPS", filters] as const,
queryFn: async ({ pageParam = 1, signal, meta }) => {
const sp = buildLauncherSearchParams(filters, pageParam);
const res = await meta!.api.get<
AxiosResponse<ListLauncherGroupsResponse>
>(`/org/${orgId}/launcher/groups?${sp.toString()}`, { signal });
return res.data.data;
},
initialPageParam: 1,
placeholderData: keepPreviousData,
getNextPageParam: (lastPage) => {
const { page, pageSize, total } = lastPage.pagination;
const nextPage = page + 1;
return page * pageSize < total ? nextPage : undefined;
}
}),
resources: (
orgId: string,
filters: LauncherQueryFilters & { groupKey: string }
) =>
infiniteQueryOptions({
queryKey: ["ORG", orgId, "LAUNCHER", "RESOURCES", filters] as const,
queryFn: async ({ pageParam = 1, signal, meta }) => {
const sp = buildLauncherSearchParams(filters, pageParam);
const res = await meta!.api.get<
AxiosResponse<ListLauncherResourcesResponse>
>(`/org/${orgId}/launcher/resources?${sp.toString()}`, {
signal
});
return res.data.data;
},
initialPageParam: 1,
placeholderData: keepPreviousData,
getNextPageParam: (lastPage) => {
const { page, pageSize, total } = lastPage.pagination;
const nextPage = page + 1;
return page * pageSize < total ? nextPage : undefined;
}
}),
scale: (orgId: string, filters: LauncherQueryFilters) =>
queryOptions({
queryKey: ["ORG", orgId, "LAUNCHER", "SCALE", filters] as const,
queryFn: async ({ signal, meta }) => {
const sp = buildLauncherSearchParams(filters, 1);
sp.delete("page");
sp.delete("pageSize");
sp.delete("groupKey");
const res = await meta!.api.get<
AxiosResponse<ListLauncherScaleResponse>
>(`/org/${orgId}/launcher/scale?${sp.toString()}`, { signal });
return res.data.data.scale;
}
}),
resourceDetail: (orgId: string, resource: LauncherResource | null) =>
queryOptions({
queryKey: [
"ORG",
orgId,
"LAUNCHER",
"RESOURCE_DETAIL",
resource?.launcherResourceKey ?? null
] as const,
enabled: resource != null,
queryFn: async ({ signal, meta }) => {
if (!resource) {
throw new Error("Resource is required");
}
if (resource.resourceType === "public") {
const res = await meta!.api.get<
AxiosResponse<GetResourceResponse>
>(`/org/${orgId}/resource/${resource.niceId}`, { signal });
const resourceData = res.data.data;
const authRes = await meta!.api.get<
AxiosResponse<GetResourceAuthInfoResponse>
>(`/resource/${resourceData.resourceGuid}/auth`, {
signal
});
return {
resourceType: "public" as const,
data: resourceData,
authInfo: authRes.data.data
};
}
const siteResourceId =
resource.siteResourceId ?? resource.resourceId;
const res = await meta!.api.get<
AxiosResponse<GetSiteResourceResponse>
>(`/org/${orgId}/site-resource/${siteResourceId}`, { signal });
return {
resourceType: "site" as const,
data: res.data.data
};
}
})
};