mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-01 20:53:56 +00:00
Compare commits
17 Commits
1.16.0-s.1
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
280cbb6e22 | ||
|
|
c20babcb53 | ||
|
|
768eebe2cd | ||
|
|
66c377a5c9 | ||
|
|
50c2aa0111 | ||
|
|
fdeb891137 | ||
|
|
6a6e3a43b1 | ||
|
|
b0a34fa21b | ||
|
|
72bf6f3c41 | ||
|
|
ad9289e0c1 | ||
|
|
b0cb0e5a99 | ||
|
|
8347203bbe | ||
|
|
4aa1186aed | ||
|
|
eed87af61d | ||
|
|
daeea8e7ea | ||
|
|
81c1a1da9c | ||
|
|
52f26396ac |
@@ -1,4 +1,5 @@
|
|||||||
FROM node:24-slim AS base
|
# FROM node:24-slim AS base
|
||||||
|
FROM public.ecr.aws/docker/library/node:24-slim AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -31,7 +32,8 @@ FROM base AS builder
|
|||||||
|
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
FROM node:24-slim AS runner
|
# FROM node:24-slim AS runner
|
||||||
|
FROM public.ecr.aws/docker/library/node:24-slim AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ services:
|
|||||||
image: fosrl/pangolin:latest
|
image: fosrl/pangolin:latest
|
||||||
container_name: pangolin
|
container_name: pangolin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1g
|
||||||
|
reservations:
|
||||||
|
memory: 256m
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ services:
|
|||||||
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
|
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
|
||||||
container_name: pangolin
|
container_name: pangolin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1g
|
||||||
|
reservations:
|
||||||
|
memory: 256m
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -1102,6 +1102,12 @@
|
|||||||
"actionGetUser": "Get User",
|
"actionGetUser": "Get User",
|
||||||
"actionGetOrgUser": "Get Organization User",
|
"actionGetOrgUser": "Get Organization User",
|
||||||
"actionListOrgDomains": "List Organization Domains",
|
"actionListOrgDomains": "List Organization Domains",
|
||||||
|
"actionGetDomain": "Get Domain",
|
||||||
|
"actionCreateOrgDomain": "Create Domain",
|
||||||
|
"actionUpdateOrgDomain": "Update Domain",
|
||||||
|
"actionDeleteOrgDomain": "Delete Domain",
|
||||||
|
"actionGetDNSRecords": "Get DNS Records",
|
||||||
|
"actionRestartOrgDomain": "Restart Domain",
|
||||||
"actionCreateSite": "Create Site",
|
"actionCreateSite": "Create Site",
|
||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
@@ -1670,10 +1676,10 @@
|
|||||||
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
|
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
|
||||||
"sshSudo": "Allow sudo",
|
"sshSudo": "Allow sudo",
|
||||||
"sshSudoCommands": "Sudo Commands",
|
"sshSudoCommands": "Sudo Commands",
|
||||||
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo.",
|
"sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo.",
|
||||||
"sshCreateHomeDir": "Create Home Directory",
|
"sshCreateHomeDir": "Create Home Directory",
|
||||||
"sshUnixGroups": "Unix Groups",
|
"sshUnixGroups": "Unix Groups",
|
||||||
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host.",
|
"sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.",
|
||||||
"retryAttempts": "Retry Attempts",
|
"retryAttempts": "Retry Attempts",
|
||||||
"expectedResponseCodes": "Expected Response Codes",
|
"expectedResponseCodes": "Expected Response Codes",
|
||||||
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ export * from "./verifyApiKeyApiKeyAccess";
|
|||||||
export * from "./verifyApiKeyClientAccess";
|
export * from "./verifyApiKeyClientAccess";
|
||||||
export * from "./verifyApiKeySiteResourceAccess";
|
export * from "./verifyApiKeySiteResourceAccess";
|
||||||
export * from "./verifyApiKeyIdpAccess";
|
export * from "./verifyApiKeyIdpAccess";
|
||||||
|
export * from "./verifyApiKeyDomainAccess";
|
||||||
|
|||||||
90
server/middlewares/integration/verifyApiKeyDomainAccess.ts
Normal file
90
server/middlewares/integration/verifyApiKeyDomainAccess.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { db, domains, orgDomains, apiKeyOrg } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
|
export async function verifyApiKeyDomainAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const domainId =
|
||||||
|
req.params.domainId || req.body.domainId || req.query.domainId;
|
||||||
|
const orgId = req.params.orgId;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domainId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid domain ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any domain in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify domain exists and belongs to the organization
|
||||||
|
const [domain] = await db
|
||||||
|
.select()
|
||||||
|
.from(domains)
|
||||||
|
.innerJoin(orgDomains, eq(orgDomains.domainId, domains.domainId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(orgDomains.domainId, domainId),
|
||||||
|
eq(orgDomains.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Domain with ID ${domainId} not found in organization ${orgId}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the API key has access to this organization
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying domain access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,12 +119,12 @@ const listClientsSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
query: z.string().optional(),
|
query: z.string().optional(),
|
||||||
sort_by: z
|
sort_by: z
|
||||||
.enum(["megabytesIn", "megabytesOut"])
|
.enum(["name", "megabytesIn", "megabytesOut"])
|
||||||
.optional()
|
.optional()
|
||||||
.catch(undefined)
|
.catch(undefined)
|
||||||
.openapi({
|
.openapi({
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["megabytesIn", "megabytesOut"],
|
enum: ["name", "megabytesIn", "megabytesOut"],
|
||||||
description: "Field to sort by"
|
description: "Field to sort by"
|
||||||
}),
|
}),
|
||||||
order: z
|
order: z
|
||||||
@@ -363,14 +363,14 @@ export async function listClients(
|
|||||||
const countQuery = db.$count(baseQuery.as("filtered_clients"));
|
const countQuery = db.$count(baseQuery.as("filtered_clients"));
|
||||||
|
|
||||||
const listMachinesQuery = baseQuery
|
const listMachinesQuery = baseQuery
|
||||||
.limit(page)
|
.limit(pageSize)
|
||||||
.offset(pageSize * (page - 1))
|
.offset(pageSize * (page - 1))
|
||||||
.orderBy(
|
.orderBy(
|
||||||
sort_by
|
sort_by
|
||||||
? order === "asc"
|
? order === "asc"
|
||||||
? asc(clients[sort_by])
|
? asc(clients[sort_by])
|
||||||
: desc(clients[sort_by])
|
: desc(clients[sort_by])
|
||||||
: asc(clients.clientId)
|
: asc(clients.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [clientsList, totalCount] = await Promise.all([
|
const [clientsList, totalCount] = await Promise.all([
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import {
|
|||||||
verifyApiKeyClientAccess,
|
verifyApiKeyClientAccess,
|
||||||
verifyApiKeySiteResourceAccess,
|
verifyApiKeySiteResourceAccess,
|
||||||
verifyApiKeySetResourceClients,
|
verifyApiKeySetResourceClients,
|
||||||
verifyLimits
|
verifyLimits,
|
||||||
|
verifyApiKeyDomainAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
@@ -347,6 +348,56 @@ authenticated.get(
|
|||||||
domain.listDomains
|
domain.listDomains
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/domain/:domainId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getDomain),
|
||||||
|
domain.getDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/domain",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createOrgDomain),
|
||||||
|
logActionAudit(ActionsEnum.createOrgDomain),
|
||||||
|
domain.createOrgDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.patch(
|
||||||
|
"/org/:orgId/domain/:domainId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateOrgDomain),
|
||||||
|
domain.updateOrgDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/domain/:domainId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.deleteOrgDomain),
|
||||||
|
logActionAudit(ActionsEnum.deleteOrgDomain),
|
||||||
|
domain.deleteAccountDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/domain/:domainId/dns-records",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getDNSRecords),
|
||||||
|
domain.getDNSRecords
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/domain/:domainId/restart",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.restartOrgDomain),
|
||||||
|
logActionAudit(ActionsEnum.restartOrgDomain),
|
||||||
|
domain.restartOrgDomain
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/invitations",
|
"/org/:orgId/invitations",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
and,
|
and,
|
||||||
asc,
|
asc,
|
||||||
count,
|
count,
|
||||||
|
desc,
|
||||||
eq,
|
eq,
|
||||||
inArray,
|
inArray,
|
||||||
isNull,
|
isNull,
|
||||||
@@ -63,6 +64,26 @@ const listResourcesSchema = z.object({
|
|||||||
description: "Page number to retrieve"
|
description: "Page number to retrieve"
|
||||||
}),
|
}),
|
||||||
query: z.string().optional(),
|
query: z.string().optional(),
|
||||||
|
sort_by: z
|
||||||
|
.enum(["name"])
|
||||||
|
.optional()
|
||||||
|
.catch(undefined)
|
||||||
|
.openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["name"],
|
||||||
|
description: "Field to sort by"
|
||||||
|
}),
|
||||||
|
order: z
|
||||||
|
.enum(["asc", "desc"])
|
||||||
|
.optional()
|
||||||
|
.default("asc")
|
||||||
|
.catch("asc")
|
||||||
|
.openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["asc", "desc"],
|
||||||
|
default: "asc",
|
||||||
|
description: "Sort order"
|
||||||
|
}),
|
||||||
enabled: z
|
enabled: z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
.transform((v) => v === "true")
|
.transform((v) => v === "true")
|
||||||
@@ -229,8 +250,16 @@ export async function listResources(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { page, pageSize, authState, enabled, query, healthStatus } =
|
const {
|
||||||
parsedQuery.data;
|
page,
|
||||||
|
pageSize,
|
||||||
|
authState,
|
||||||
|
enabled,
|
||||||
|
query,
|
||||||
|
healthStatus,
|
||||||
|
sort_by,
|
||||||
|
order
|
||||||
|
} = parsedQuery.data;
|
||||||
|
|
||||||
const parsedParams = listResourcesParamsSchema.safeParse(req.params);
|
const parsedParams = listResourcesParamsSchema.safeParse(req.params);
|
||||||
if (!parsedParams.success) {
|
if (!parsedParams.success) {
|
||||||
@@ -395,7 +424,13 @@ export async function listResources(
|
|||||||
baseQuery
|
baseQuery
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
.offset(pageSize * (page - 1))
|
.offset(pageSize * (page - 1))
|
||||||
.orderBy(asc(resources.resourceId)),
|
.orderBy(
|
||||||
|
sort_by
|
||||||
|
? order === "asc"
|
||||||
|
? asc(resources[sort_by])
|
||||||
|
: desc(resources[sort_by])
|
||||||
|
: asc(resources.name)
|
||||||
|
),
|
||||||
countQuery
|
countQuery
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -108,12 +108,12 @@ const listSitesSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
query: z.string().optional(),
|
query: z.string().optional(),
|
||||||
sort_by: z
|
sort_by: z
|
||||||
.enum(["megabytesIn", "megabytesOut"])
|
.enum(["name", "megabytesIn", "megabytesOut"])
|
||||||
.optional()
|
.optional()
|
||||||
.catch(undefined)
|
.catch(undefined)
|
||||||
.openapi({
|
.openapi({
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["megabytesIn", "megabytesOut"],
|
enum: ["name", "megabytesIn", "megabytesOut"],
|
||||||
description: "Field to sort by"
|
description: "Field to sort by"
|
||||||
}),
|
}),
|
||||||
order: z
|
order: z
|
||||||
@@ -278,7 +278,7 @@ export async function listSites(
|
|||||||
|
|
||||||
// we need to add `as` so that drizzle filters the result as a subquery
|
// we need to add `as` so that drizzle filters the result as a subquery
|
||||||
const countQuery = db.$count(
|
const countQuery = db.$count(
|
||||||
querySitesBase().where(and(...conditions))
|
querySitesBase().where(and(...conditions)).as("filtered_sites")
|
||||||
);
|
);
|
||||||
|
|
||||||
const siteListQuery = baseQuery
|
const siteListQuery = baseQuery
|
||||||
@@ -289,7 +289,7 @@ export async function listSites(
|
|||||||
? order === "asc"
|
? order === "asc"
|
||||||
? asc(sites[sort_by])
|
? asc(sites[sort_by])
|
||||||
: desc(sites[sort_by])
|
: desc(sites[sort_by])
|
||||||
: asc(sites.siteId)
|
: asc(sites.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [totalCount, rows] = await Promise.all([
|
const [totalCount, rows] = await Promise.all([
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import logger from "@server/logger";
|
|||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import type { PaginatedResponse } from "@server/types/Pagination";
|
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||||
import { and, asc, eq, like, or, sql } from "drizzle-orm";
|
import { and, asc, desc, eq, like, or, sql } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -48,6 +48,26 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({
|
|||||||
type: "string",
|
type: "string",
|
||||||
enum: ["host", "cidr"],
|
enum: ["host", "cidr"],
|
||||||
description: "Filter site resources by mode"
|
description: "Filter site resources by mode"
|
||||||
|
}),
|
||||||
|
sort_by: z
|
||||||
|
.enum(["name"])
|
||||||
|
.optional()
|
||||||
|
.catch(undefined)
|
||||||
|
.openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["name"],
|
||||||
|
description: "Field to sort by"
|
||||||
|
}),
|
||||||
|
order: z
|
||||||
|
.enum(["asc", "desc"])
|
||||||
|
.optional()
|
||||||
|
.default("asc")
|
||||||
|
.catch("asc")
|
||||||
|
.openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["asc", "desc"],
|
||||||
|
default: "asc",
|
||||||
|
description: "Sort order"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,7 +151,8 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const { page, pageSize, query, mode } = parsedQuery.data;
|
const { page, pageSize, query, mode, sort_by, order } =
|
||||||
|
parsedQuery.data;
|
||||||
|
|
||||||
const conditions = [and(eq(siteResources.orgId, orgId))];
|
const conditions = [and(eq(siteResources.orgId, orgId))];
|
||||||
if (query) {
|
if (query) {
|
||||||
@@ -172,14 +193,20 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
const baseQuery = querySiteResourcesBase().where(and(...conditions));
|
const baseQuery = querySiteResourcesBase().where(and(...conditions));
|
||||||
|
|
||||||
const countQuery = db.$count(
|
const countQuery = db.$count(
|
||||||
querySiteResourcesBase().where(and(...conditions))
|
querySiteResourcesBase().where(and(...conditions)).as("filtered_site_resources")
|
||||||
);
|
);
|
||||||
|
|
||||||
const [siteResourcesList, totalCount] = await Promise.all([
|
const [siteResourcesList, totalCount] = await Promise.all([
|
||||||
baseQuery
|
baseQuery
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
.offset(pageSize * (page - 1))
|
.offset(pageSize * (page - 1))
|
||||||
.orderBy(asc(siteResources.siteResourceId)),
|
.orderBy(
|
||||||
|
sort_by
|
||||||
|
? order === "asc"
|
||||||
|
? asc(siteResources[sort_by])
|
||||||
|
: desc(siteResources[sort_by])
|
||||||
|
: asc(siteResources.name)
|
||||||
|
),
|
||||||
countQuery
|
countQuery
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { siteResources, sites, SiteResource } from "@server/db";
|
|||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { and, asc, desc, eq } from "drizzle-orm";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
@@ -27,7 +27,27 @@ const listSiteResourcesQuerySchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.default("0")
|
.default("0")
|
||||||
.transform(Number)
|
.transform(Number)
|
||||||
.pipe(z.int().nonnegative())
|
.pipe(z.int().nonnegative()),
|
||||||
|
sort_by: z
|
||||||
|
.enum(["name"])
|
||||||
|
.optional()
|
||||||
|
.catch(undefined)
|
||||||
|
.openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["name"],
|
||||||
|
description: "Field to sort by"
|
||||||
|
}),
|
||||||
|
order: z
|
||||||
|
.enum(["asc", "desc"])
|
||||||
|
.optional()
|
||||||
|
.default("asc")
|
||||||
|
.catch("asc")
|
||||||
|
.openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["asc", "desc"],
|
||||||
|
default: "asc",
|
||||||
|
description: "Sort order"
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ListSiteResourcesResponse = {
|
export type ListSiteResourcesResponse = {
|
||||||
@@ -75,7 +95,7 @@ export async function listSiteResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { siteId, orgId } = parsedParams.data;
|
const { siteId, orgId } = parsedParams.data;
|
||||||
const { limit, offset } = parsedQuery.data;
|
const { limit, offset, sort_by, order } = parsedQuery.data;
|
||||||
|
|
||||||
// Verify the site exists and belongs to the org
|
// Verify the site exists and belongs to the org
|
||||||
const site = await db
|
const site = await db
|
||||||
@@ -98,6 +118,13 @@ export async function listSiteResources(
|
|||||||
eq(siteResources.orgId, orgId)
|
eq(siteResources.orgId, orgId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.orderBy(
|
||||||
|
sort_by
|
||||||
|
? order === "asc"
|
||||||
|
? asc(siteResources[sort_by])
|
||||||
|
: desc(siteResources[sort_by])
|
||||||
|
: asc(siteResources.name)
|
||||||
|
)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,14 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { use, useActionState, useCallback, useEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
use,
|
||||||
|
useActionState,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState
|
||||||
|
} from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -184,7 +191,8 @@ function ProxyResourceTargetsForm({
|
|||||||
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
|
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshContainersForSite = useCallback(async (siteId: number) => {
|
const refreshContainersForSite = useCallback(
|
||||||
|
async (siteId: number) => {
|
||||||
const dockerManager = new DockerManager(api, siteId);
|
const dockerManager = new DockerManager(api, siteId);
|
||||||
const containers = await dockerManager.fetchContainers();
|
const containers = await dockerManager.fetchContainers();
|
||||||
|
|
||||||
@@ -196,9 +204,12 @@ function ProxyResourceTargetsForm({
|
|||||||
}
|
}
|
||||||
return newMap;
|
return newMap;
|
||||||
});
|
});
|
||||||
}, [api]);
|
},
|
||||||
|
[api]
|
||||||
|
);
|
||||||
|
|
||||||
const getDockerStateForSite = useCallback((siteId: number): DockerState => {
|
const getDockerStateForSite = useCallback(
|
||||||
|
(siteId: number): DockerState => {
|
||||||
return (
|
return (
|
||||||
dockerStates.get(siteId) || {
|
dockerStates.get(siteId) || {
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
@@ -206,7 +217,9 @@ function ProxyResourceTargetsForm({
|
|||||||
containers: []
|
containers: []
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [dockerStates]);
|
},
|
||||||
|
[dockerStates]
|
||||||
|
);
|
||||||
|
|
||||||
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -220,7 +233,9 @@ function ProxyResourceTargetsForm({
|
|||||||
|
|
||||||
const removeTarget = useCallback((targetId: number) => {
|
const removeTarget = useCallback((targetId: number) => {
|
||||||
setTargets((prevTargets) => {
|
setTargets((prevTargets) => {
|
||||||
const targetToRemove = prevTargets.find((target) => target.targetId === targetId);
|
const targetToRemove = prevTargets.find(
|
||||||
|
(target) => target.targetId === targetId
|
||||||
|
);
|
||||||
if (targetToRemove && !targetToRemove.new) {
|
if (targetToRemove && !targetToRemove.new) {
|
||||||
setTargetsToRemove((prev) => [...prev, targetId]);
|
setTargetsToRemove((prev) => [...prev, targetId]);
|
||||||
}
|
}
|
||||||
@@ -228,7 +243,8 @@ function ProxyResourceTargetsForm({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateTarget = useCallback((targetId: number, data: Partial<LocalTarget>) => {
|
const updateTarget = useCallback(
|
||||||
|
(targetId: number, data: Partial<LocalTarget>) => {
|
||||||
setTargets((prevTargets) => {
|
setTargets((prevTargets) => {
|
||||||
const site = sites.find((site) => site.siteId === data.siteId);
|
const site = sites.find((site) => site.siteId === data.siteId);
|
||||||
return prevTargets.map((target) =>
|
return prevTargets.map((target) =>
|
||||||
@@ -242,7 +258,9 @@ function ProxyResourceTargetsForm({
|
|||||||
: target
|
: target
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [sites]);
|
},
|
||||||
|
[sites]
|
||||||
|
);
|
||||||
|
|
||||||
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
|
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
|
||||||
setSelectedTargetForHealthCheck(target);
|
setSelectedTargetForHealthCheck(target);
|
||||||
@@ -250,7 +268,6 @@ function ProxyResourceTargetsForm({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
|
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
|
||||||
|
|
||||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||||
id: "priority",
|
id: "priority",
|
||||||
header: () => (
|
header: () => (
|
||||||
@@ -581,7 +598,17 @@ function ProxyResourceTargetsForm({
|
|||||||
actionsColumn
|
actionsColumn
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}, [isAdvancedMode, isHttp, sites, updateTarget, getDockerStateForSite, refreshContainersForSite, openHealthCheckDialog, removeTarget, t]);
|
}, [
|
||||||
|
isAdvancedMode,
|
||||||
|
isHttp,
|
||||||
|
sites,
|
||||||
|
updateTarget,
|
||||||
|
getDockerStateForSite,
|
||||||
|
refreshContainersForSite,
|
||||||
|
openHealthCheckDialog,
|
||||||
|
removeTarget,
|
||||||
|
t
|
||||||
|
]);
|
||||||
|
|
||||||
function addNewTarget() {
|
function addNewTarget() {
|
||||||
const isHttp = resource.http;
|
const isHttp = resource.http;
|
||||||
|
|||||||
@@ -15,7 +15,15 @@ import { InfoPopup } from "@app/components/ui/info-popup";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { ArrowUpDown, ArrowUpRight, MoreHorizontal } from "lucide-react";
|
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
||||||
|
import {
|
||||||
|
ArrowDown01Icon,
|
||||||
|
ArrowUp10Icon,
|
||||||
|
ArrowUpDown,
|
||||||
|
ArrowUpRight,
|
||||||
|
ChevronsUpDownIcon,
|
||||||
|
MoreHorizontal
|
||||||
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -133,7 +141,26 @@ export default function ClientResourcesTable({
|
|||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
friendlyName: t("name"),
|
friendlyName: t("name"),
|
||||||
header: () => <span className="p-3">{t("name")}</span>
|
header: () => {
|
||||||
|
const nameOrder = getSortDirection("name", searchParams);
|
||||||
|
const Icon =
|
||||||
|
nameOrder === "asc"
|
||||||
|
? ArrowDown01Icon
|
||||||
|
: nameOrder === "desc"
|
||||||
|
? ArrowUp10Icon
|
||||||
|
: ChevronsUpDownIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="p-3"
|
||||||
|
onClick={() => toggleSort("name")}
|
||||||
|
>
|
||||||
|
{t("name")}
|
||||||
|
<Icon className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "niceId",
|
id: "niceId",
|
||||||
@@ -329,6 +356,14 @@ export default function ClientResourcesTable({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSort(column: string) {
|
||||||
|
const newSearch = getNextSortOrder(column, searchParams);
|
||||||
|
|
||||||
|
filter({
|
||||||
|
searchParams: newSearch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handlePaginationChange = (newPage: PaginationState) => {
|
const handlePaginationChange = (newPage: PaginationState) => {
|
||||||
searchParams.set("page", (newPage.pageIndex + 1).toString());
|
searchParams.set("page", (newPage.pageIndex + 1).toString());
|
||||||
searchParams.set("pageSize", newPage.pageSize.toString());
|
searchParams.set("pageSize", newPage.pageSize.toString());
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useEffect, useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
@@ -39,7 +39,8 @@ import { formatAxiosError } from "@app/lib/api";
|
|||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { ListResourcesResponse } from "@server/routers/resource";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { orgQueries } from "@app/lib/queries";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -94,14 +95,22 @@ export default function CreateShareLinkForm({
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const [resources, setResources] = useState<
|
const { data: allResources = [] } = useQuery(
|
||||||
{
|
orgQueries.resources({ orgId: org?.org.orgId ?? "" })
|
||||||
resourceId: number;
|
);
|
||||||
name: string;
|
|
||||||
niceId: string;
|
const resources = useMemo(
|
||||||
resourceUrl: string;
|
() =>
|
||||||
}[]
|
allResources
|
||||||
>([]);
|
.filter((r) => r.http)
|
||||||
|
.map((r) => ({
|
||||||
|
resourceId: r.resourceId,
|
||||||
|
name: r.name,
|
||||||
|
niceId: r.niceId,
|
||||||
|
resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/`
|
||||||
|
})),
|
||||||
|
[allResources]
|
||||||
|
);
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
resourceId: z.number({ message: t("shareErrorSelectResource") }),
|
resourceId: z.number({ message: t("shareErrorSelectResource") }),
|
||||||
@@ -130,47 +139,6 @@ export default function CreateShareLinkForm({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchResources() {
|
|
||||||
const res = await api
|
|
||||||
.get<
|
|
||||||
AxiosResponse<ListResourcesResponse>
|
|
||||||
>(`/org/${org?.org.orgId}/resources`)
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("shareErrorFetchResource"),
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
t("shareErrorFetchResourceDescription")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res?.status === 200) {
|
|
||||||
setResources(
|
|
||||||
res.data.data.resources
|
|
||||||
.filter((r) => {
|
|
||||||
return r.http;
|
|
||||||
})
|
|
||||||
.map((r) => ({
|
|
||||||
resourceId: r.resourceId,
|
|
||||||
name: r.name,
|
|
||||||
niceId: r.niceId,
|
|
||||||
resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/`
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchResources();
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
|||||||
@@ -1230,8 +1230,12 @@ export function InternalResourceForm({
|
|||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<StrategySelect<"site" | "remote">
|
<StrategySelect<
|
||||||
value={field.value ?? undefined}
|
"site" | "remote"
|
||||||
|
>
|
||||||
|
value={
|
||||||
|
field.value ?? undefined
|
||||||
|
}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
id: "site",
|
id: "site",
|
||||||
@@ -1241,7 +1245,8 @@ export function InternalResourceForm({
|
|||||||
description: t(
|
description: t(
|
||||||
"internalResourceAuthDaemonSiteDescription"
|
"internalResourceAuthDaemonSiteDescription"
|
||||||
),
|
),
|
||||||
disabled: sshSectionDisabled
|
disabled:
|
||||||
|
sshSectionDisabled
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "remote",
|
id: "remote",
|
||||||
@@ -1251,11 +1256,13 @@ export function InternalResourceForm({
|
|||||||
description: t(
|
description: t(
|
||||||
"internalResourceAuthDaemonRemoteDescription"
|
"internalResourceAuthDaemonRemoteDescription"
|
||||||
),
|
),
|
||||||
disabled: sshSectionDisabled
|
disabled:
|
||||||
|
sshSectionDisabled
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
if (sshSectionDisabled) return;
|
if (sshSectionDisabled)
|
||||||
|
return;
|
||||||
field.onChange(v);
|
field.onChange(v);
|
||||||
if (v === "site") {
|
if (v === "site") {
|
||||||
form.setValue(
|
form.setValue(
|
||||||
@@ -1289,10 +1296,17 @@ export function InternalResourceForm({
|
|||||||
max={65535}
|
max={65535}
|
||||||
placeholder="22123"
|
placeholder="22123"
|
||||||
{...field}
|
{...field}
|
||||||
disabled={sshSectionDisabled}
|
disabled={
|
||||||
value={field.value ?? ""}
|
sshSectionDisabled
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
field.value ?? ""
|
||||||
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (sshSectionDisabled) return;
|
if (
|
||||||
|
sshSectionDisabled
|
||||||
|
)
|
||||||
|
return;
|
||||||
const v =
|
const v =
|
||||||
e.target.value;
|
e.target.value;
|
||||||
if (v === "") {
|
if (v === "") {
|
||||||
@@ -1301,12 +1315,12 @@ export function InternalResourceForm({
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const num = parseInt(
|
const num =
|
||||||
v,
|
parseInt(v, 10);
|
||||||
10
|
|
||||||
);
|
|
||||||
field.onChange(
|
field.onChange(
|
||||||
Number.isNaN(num)
|
Number.isNaN(
|
||||||
|
num
|
||||||
|
)
|
||||||
? null
|
? null
|
||||||
: num
|
: num
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -69,15 +69,16 @@ export function LayoutMobileMenu({
|
|||||||
<SheetDescription className="sr-only">
|
<SheetDescription className="sr-only">
|
||||||
{t("navbarDescription")}
|
{t("navbarDescription")}
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
<div className="flex-1 overflow-y-auto relative">
|
<div className="w-full border-b border-border">
|
||||||
<div className="px-1">
|
<div className="px-1 shrink-0">
|
||||||
<OrgSelector
|
<OrgSelector
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
orgs={orgs}
|
orgs={orgs}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full border-b border-border" />
|
</div>
|
||||||
<div className="px-3 pt-3">
|
<div className="flex-1 overflow-y-auto relative">
|
||||||
|
<div className="px-3">
|
||||||
{!isAdminPage &&
|
{!isAdminPage &&
|
||||||
user.serverAdmin && (
|
user.serverAdmin && (
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
|
|||||||
@@ -204,7 +204,26 @@ export default function MachineClientsTable({
|
|||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
friendlyName: t("name"),
|
friendlyName: t("name"),
|
||||||
header: () => <span className="px-3">{t("name")}</span>,
|
header: () => {
|
||||||
|
const nameOrder = getSortDirection("name", searchParams);
|
||||||
|
const Icon =
|
||||||
|
nameOrder === "asc"
|
||||||
|
? ArrowDown01Icon
|
||||||
|
: nameOrder === "desc"
|
||||||
|
? ArrowUp10Icon
|
||||||
|
: ChevronsUpDownIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => toggleSort("name")}
|
||||||
|
className="px-3"
|
||||||
|
>
|
||||||
|
{t("name")}
|
||||||
|
<Icon className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t("actionListInvitations")]: "listInvitations",
|
[t("actionListInvitations")]: "listInvitations",
|
||||||
[t("actionRemoveUser")]: "removeUser",
|
[t("actionRemoveUser")]: "removeUser",
|
||||||
[t("actionListUsers")]: "listUsers",
|
[t("actionListUsers")]: "listUsers",
|
||||||
[t("actionListOrgDomains")]: "listOrgDomains",
|
|
||||||
[t("updateOrgUser")]: "updateOrgUser",
|
[t("updateOrgUser")]: "updateOrgUser",
|
||||||
[t("createOrgUser")]: "createOrgUser",
|
[t("createOrgUser")]: "createOrgUser",
|
||||||
[t("actionApplyBlueprint")]: "applyBlueprint",
|
[t("actionApplyBlueprint")]: "applyBlueprint",
|
||||||
@@ -39,6 +38,16 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t("actionGetBlueprint")]: "getBlueprint"
|
[t("actionGetBlueprint")]: "getBlueprint"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Domain: {
|
||||||
|
[t("actionListOrgDomains")]: "listOrgDomains",
|
||||||
|
[t("actionGetDomain")]: "getDomain",
|
||||||
|
[t("actionCreateOrgDomain")]: "createOrgDomain",
|
||||||
|
[t("actionUpdateOrgDomain")]: "updateOrgDomain",
|
||||||
|
[t("actionDeleteOrgDomain")]: "deleteOrgDomain",
|
||||||
|
[t("actionGetDNSRecords")]: "getDNSRecords",
|
||||||
|
[t("actionRestartOrgDomain")]: "restartOrgDomain"
|
||||||
|
},
|
||||||
|
|
||||||
Site: {
|
Site: {
|
||||||
[t("actionCreateSite")]: "createSite",
|
[t("actionCreateSite")]: "createSite",
|
||||||
[t("actionDeleteSite")]: "deleteSite",
|
[t("actionDeleteSite")]: "deleteSite",
|
||||||
|
|||||||
@@ -14,15 +14,19 @@ import { InfoPopup } from "@app/components/ui/info-popup";
|
|||||||
import { Switch } from "@app/components/ui/switch";
|
import { Switch } from "@app/components/ui/switch";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
|
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { UpdateResourceResponse } from "@server/routers/resource";
|
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||||
import type { PaginationState } from "@tanstack/react-table";
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import {
|
import {
|
||||||
|
ArrowDown01Icon,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
|
ArrowUp10Icon,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
ChevronsUpDownIcon,
|
||||||
Clock,
|
Clock,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
@@ -318,7 +322,26 @@ export default function ProxyResourcesTable({
|
|||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
friendlyName: t("name"),
|
friendlyName: t("name"),
|
||||||
header: () => <span className="p-3">{t("name")}</span>
|
header: () => {
|
||||||
|
const nameOrder = getSortDirection("name", searchParams);
|
||||||
|
const Icon =
|
||||||
|
nameOrder === "asc"
|
||||||
|
? ArrowDown01Icon
|
||||||
|
: nameOrder === "desc"
|
||||||
|
? ArrowUp10Icon
|
||||||
|
: ChevronsUpDownIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="p-3"
|
||||||
|
onClick={() => toggleSort("name")}
|
||||||
|
>
|
||||||
|
{t("name")}
|
||||||
|
<Icon className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "niceId",
|
id: "niceId",
|
||||||
@@ -563,6 +586,14 @@ export default function ProxyResourcesTable({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSort(column: string) {
|
||||||
|
const newSearch = getNextSortOrder(column, searchParams);
|
||||||
|
|
||||||
|
filter({
|
||||||
|
searchParams: newSearch
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handlePaginationChange = (newPage: PaginationState) => {
|
const handlePaginationChange = (newPage: PaginationState) => {
|
||||||
searchParams.set("page", (newPage.pageIndex + 1).toString());
|
searchParams.set("page", (newPage.pageIndex + 1).toString());
|
||||||
searchParams.set("pageSize", newPage.pageSize.toString());
|
searchParams.set("pageSize", newPage.pageSize.toString());
|
||||||
|
|||||||
@@ -141,7 +141,24 @@ export default function SitesTable({
|
|||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
header: () => {
|
header: () => {
|
||||||
return <span className="p-3">{t("name")}</span>;
|
const nameOrder = getSortDirection("name", searchParams);
|
||||||
|
const Icon =
|
||||||
|
nameOrder === "asc"
|
||||||
|
? ArrowDown01Icon
|
||||||
|
: nameOrder === "desc"
|
||||||
|
? ArrowUp10Icon
|
||||||
|
: ChevronsUpDownIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="p-3"
|
||||||
|
onClick={() => toggleSort("name")}
|
||||||
|
>
|
||||||
|
{t("name")}
|
||||||
|
<Icon className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import type { ListClientsResponse } from "@server/routers/client";
|
|||||||
import type { ListDomainsResponse } from "@server/routers/domain";
|
import type { ListDomainsResponse } from "@server/routers/domain";
|
||||||
import type {
|
import type {
|
||||||
GetResourceWhitelistResponse,
|
GetResourceWhitelistResponse,
|
||||||
ListResourceNamesResponse
|
ListResourceNamesResponse,
|
||||||
|
ListResourcesResponse
|
||||||
} from "@server/routers/resource";
|
} from "@server/routers/resource";
|
||||||
import type { ListRolesResponse } from "@server/routers/role";
|
import type { ListRolesResponse } from "@server/routers/role";
|
||||||
import type { ListSitesResponse } from "@server/routers/site";
|
import type { ListSitesResponse } from "@server/routers/site";
|
||||||
@@ -90,23 +91,13 @@ export const productUpdatesQueries = {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clientFilterSchema = z.object({
|
|
||||||
pageSize: z.int().prefault(1000).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const orgQueries = {
|
export const orgQueries = {
|
||||||
clients: ({
|
clients: ({ orgId }: { orgId: string }) =>
|
||||||
orgId,
|
|
||||||
filters
|
|
||||||
}: {
|
|
||||||
orgId: string;
|
|
||||||
filters?: z.infer<typeof clientFilterSchema>;
|
|
||||||
}) =>
|
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["ORG", orgId, "CLIENTS", filters] as const,
|
queryKey: ["ORG", orgId, "CLIENTS"] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const sp = new URLSearchParams({
|
const sp = new URLSearchParams({
|
||||||
pageSize: (filters?.pageSize ?? 1000).toString()
|
pageSize: "10000"
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
@@ -143,9 +134,13 @@ export const orgQueries = {
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["ORG", orgId, "SITES"] as const,
|
queryKey: ["ORG", orgId, "SITES"] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const sp = new URLSearchParams({
|
||||||
|
pageSize: "10000"
|
||||||
|
});
|
||||||
|
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListSitesResponse>
|
AxiosResponse<ListSitesResponse>
|
||||||
>(`/org/${orgId}/sites`, { signal });
|
>(`/org/${orgId}/sites?${sp.toString()}`, { signal });
|
||||||
return res.data.data.sites;
|
return res.data.data.sites;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -182,6 +177,22 @@ export const orgQueries = {
|
|||||||
);
|
);
|
||||||
return res.data.data.idps;
|
return res.data.data.idps;
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
resources: ({ orgId }: { orgId: string }) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["ORG", orgId, "RESOURCES"] as const,
|
||||||
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const sp = new URLSearchParams({
|
||||||
|
pageSize: "10000"
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await meta!.api.get<
|
||||||
|
AxiosResponse<ListResourcesResponse>
|
||||||
|
>(`/org/${orgId}/resources?${sp.toString()}`, { signal });
|
||||||
|
|
||||||
|
return res.data.data.resources;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user