mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-23 02:56:37 +00:00
Update crud endpoints and ui
This commit is contained in:
@@ -63,10 +63,12 @@ const bodySchema = z
|
|||||||
export type SignSshKeyResponse = {
|
export type SignSshKeyResponse = {
|
||||||
certificate: string;
|
certificate: string;
|
||||||
messageIds: number[];
|
messageIds: number[];
|
||||||
|
messageId: number;
|
||||||
sshUsername: string;
|
sshUsername: string;
|
||||||
sshHost: string;
|
sshHost: string;
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
siteIds: number[];
|
siteIds: number[];
|
||||||
|
siteId: number;
|
||||||
keyId: string;
|
keyId: string;
|
||||||
validPrincipals: string[];
|
validPrincipals: string[];
|
||||||
validAfter: string;
|
validAfter: string;
|
||||||
@@ -472,16 +474,16 @@ export async function signSshKey(
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: WE NEED TO MAKE SURE THE MESSAGEIDS ARE BACKWARD COMPATABILE AND THE SITEIDS TOO AND UPDATE THE CLI TO HANDLE THE MESSAGE IDS
|
|
||||||
|
|
||||||
return response<SignSshKeyResponse>(res, {
|
return response<SignSshKeyResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
certificate: cert.certificate,
|
certificate: cert.certificate,
|
||||||
messageIds: messageIds,
|
messageIds: messageIds,
|
||||||
|
messageId: messageIds[0], // just pick the first one for backward compatibility
|
||||||
sshUsername: usernameToUse,
|
sshUsername: usernameToUse,
|
||||||
sshHost: sshHost,
|
sshHost: sshHost,
|
||||||
resourceId: resource.siteResourceId,
|
resourceId: resource.siteResourceId,
|
||||||
siteIds: siteIds,
|
siteIds: siteIds,
|
||||||
|
siteId: siteIds[0], // just pick the first one for backward compatibility
|
||||||
keyId: cert.keyId,
|
keyId: cert.keyId,
|
||||||
validPrincipals: cert.validPrincipals,
|
validPrincipals: cert.validPrincipals,
|
||||||
validAfter: cert.validAfter.toISOString(),
|
validAfter: cert.validAfter.toISOString(),
|
||||||
|
|||||||
@@ -17,38 +17,34 @@ const getSiteResourceParamsSchema = z.strictObject({
|
|||||||
.transform((val) => (val ? Number(val) : undefined))
|
.transform((val) => (val ? Number(val) : undefined))
|
||||||
.pipe(z.int().positive().optional())
|
.pipe(z.int().positive().optional())
|
||||||
.optional(),
|
.optional(),
|
||||||
siteId: z.string().transform(Number).pipe(z.int().positive()),
|
|
||||||
niceId: z.string().optional(),
|
niceId: z.string().optional(),
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
async function query(
|
async function query(
|
||||||
siteResourceId?: number,
|
siteResourceId?: number,
|
||||||
siteId?: number,
|
|
||||||
niceId?: string,
|
niceId?: string,
|
||||||
orgId?: string
|
orgId?: string
|
||||||
) {
|
) {
|
||||||
if (siteResourceId && siteId && orgId) {
|
if (siteResourceId && orgId) {
|
||||||
const [siteResource] = await db
|
const [siteResource] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(siteResources.siteResourceId, siteResourceId),
|
eq(siteResources.siteResourceId, siteResourceId),
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
eq(siteResources.orgId, orgId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
return siteResource;
|
return siteResource;
|
||||||
} else if (niceId && siteId && orgId) {
|
} else if (niceId && orgId) {
|
||||||
const [siteResource] = await db
|
const [siteResource] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(siteResources.niceId, niceId),
|
eq(siteResources.niceId, niceId),
|
||||||
eq(siteResources.siteId, siteId),
|
|
||||||
eq(siteResources.orgId, orgId)
|
eq(siteResources.orgId, orgId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -84,7 +80,6 @@ registry.registerPath({
|
|||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
niceId: z.string(),
|
niceId: z.string(),
|
||||||
siteId: z.number(),
|
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -107,10 +102,10 @@ export async function getSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteResourceId, siteId, niceId, orgId } = parsedParams.data;
|
const { siteResourceId, niceId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
// Get the site resource
|
// Get the site resource
|
||||||
const siteResource = await query(siteResourceId, siteId, niceId, orgId);
|
const siteResource = await query(siteResourceId, niceId, orgId);
|
||||||
|
|
||||||
if (!siteResource) {
|
if (!siteResource) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({
|
|||||||
|
|
||||||
export type ListAllSiteResourcesByOrgResponse = PaginatedResponse<{
|
export type ListAllSiteResourcesByOrgResponse = PaginatedResponse<{
|
||||||
siteResources: (SiteResource & {
|
siteResources: (SiteResource & {
|
||||||
|
siteIds: number[];
|
||||||
siteNames: string[];
|
siteNames: string[];
|
||||||
siteNiceIds: string[];
|
siteNiceIds: string[];
|
||||||
siteAddresses: (string | null)[];
|
siteAddresses: (string | null)[];
|
||||||
@@ -103,6 +104,7 @@ function querySiteResourcesBase() {
|
|||||||
defaultNetworkId: siteResources.defaultNetworkId,
|
defaultNetworkId: siteResources.defaultNetworkId,
|
||||||
siteNames: sql<string[]>`array_agg(${sites.name})`,
|
siteNames: sql<string[]>`array_agg(${sites.name})`,
|
||||||
siteNiceIds: sql<string[]>`array_agg(${sites.niceId})`,
|
siteNiceIds: sql<string[]>`array_agg(${sites.niceId})`,
|
||||||
|
siteIds: sql<number[]>`array_agg(${sites.siteId})`,
|
||||||
siteAddresses: sql<(string | null)[]>`array_agg(${sites.address})`
|
siteAddresses: sql<(string | null)[]>`array_agg(${sites.address})`
|
||||||
})
|
})
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, networks, siteNetworks } from "@server/db";
|
||||||
import { siteResources, sites, SiteResource } from "@server/db";
|
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";
|
||||||
@@ -108,13 +108,21 @@ export async function listSiteResources(
|
|||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
|
return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get site resources
|
// Get site resources by joining networks to siteResources via siteNetworks
|
||||||
const siteResourcesList = await db
|
const siteResourcesList = await db
|
||||||
.select()
|
.select()
|
||||||
.from(siteResources)
|
.from(siteNetworks)
|
||||||
|
.innerJoin(
|
||||||
|
networks,
|
||||||
|
eq(siteNetworks.networkId, networks.networkId)
|
||||||
|
)
|
||||||
|
.innerJoin(
|
||||||
|
siteResources,
|
||||||
|
eq(siteResources.networkId, networks.networkId)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(siteResources.siteId, siteId),
|
eq(siteNetworks.siteId, siteId),
|
||||||
eq(siteResources.orgId, orgId)
|
eq(siteResources.orgId, orgId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -128,6 +136,7 @@ export async function listSiteResources(
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
|
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: { siteResources: siteResourcesList },
|
data: { siteResources: siteResourcesList },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -60,17 +60,17 @@ export default async function ClientResourcesPage(
|
|||||||
id: siteResource.siteResourceId,
|
id: siteResource.siteResourceId,
|
||||||
name: siteResource.name,
|
name: siteResource.name,
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
siteName: siteResource.siteName,
|
siteNames: siteResource.siteNames,
|
||||||
siteAddress: siteResource.siteAddress || null,
|
siteAddresses: siteResource.siteAddresses || null,
|
||||||
mode: siteResource.mode || ("port" as any),
|
mode: siteResource.mode || ("port" as any),
|
||||||
// protocol: siteResource.protocol,
|
// protocol: siteResource.protocol,
|
||||||
// proxyPort: siteResource.proxyPort,
|
// proxyPort: siteResource.proxyPort,
|
||||||
siteId: siteResource.siteId,
|
siteIds: siteResource.siteIds,
|
||||||
destination: siteResource.destination,
|
destination: siteResource.destination,
|
||||||
// destinationPort: siteResource.destinationPort,
|
// destinationPort: siteResource.destinationPort,
|
||||||
alias: siteResource.alias || null,
|
alias: siteResource.alias || null,
|
||||||
aliasAddress: siteResource.aliasAddress || null,
|
aliasAddress: siteResource.aliasAddress || null,
|
||||||
siteNiceId: siteResource.siteNiceId,
|
siteNiceIds: siteResource.siteNiceIds,
|
||||||
niceId: siteResource.niceId,
|
niceId: siteResource.niceId,
|
||||||
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
||||||
udpPortRangeString: siteResource.udpPortRangeString || null,
|
udpPortRangeString: siteResource.udpPortRangeString || null,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
ArrowUp10Icon,
|
ArrowUp10Icon,
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
|
ChevronDown,
|
||||||
ChevronsUpDownIcon,
|
ChevronsUpDownIcon,
|
||||||
MoreHorizontal
|
MoreHorizontal
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -43,14 +44,14 @@ export type InternalResourceRow = {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
siteName: string;
|
siteNames: string[];
|
||||||
siteAddress: string | null;
|
siteAddresses: (string | null)[];
|
||||||
|
siteIds: number[];
|
||||||
|
siteNiceIds: string[];
|
||||||
// mode: "host" | "cidr" | "port";
|
// mode: "host" | "cidr" | "port";
|
||||||
mode: "host" | "cidr";
|
mode: "host" | "cidr";
|
||||||
// protocol: string | null;
|
// protocol: string | null;
|
||||||
// proxyPort: number | null;
|
// proxyPort: number | null;
|
||||||
siteId: number;
|
|
||||||
siteNiceId: string;
|
|
||||||
destination: string;
|
destination: string;
|
||||||
// destinationPort: number | null;
|
// destinationPort: number | null;
|
||||||
alias: string | null;
|
alias: string | null;
|
||||||
@@ -136,6 +137,60 @@ export default function ClientResourcesTable({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function SiteCell({ resourceRow }: { resourceRow: InternalResourceRow }) {
|
||||||
|
const { siteNames, siteNiceIds, orgId } = resourceRow;
|
||||||
|
|
||||||
|
if (!siteNames || siteNames.length === 0) {
|
||||||
|
return <span>-</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siteNames.length === 1) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/${orgId}/settings/sites/${siteNiceIds[0]}`}
|
||||||
|
>
|
||||||
|
<Button variant="outline">
|
||||||
|
{siteNames[0]}
|
||||||
|
<ArrowUpRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{siteNames.length} {t("sites")}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
{siteNames.map((siteName, idx) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={siteNiceIds[idx]}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/${orgId}/settings/sites/${siteNiceIds[idx]}`}
|
||||||
|
className="flex items-center gap-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
{siteName}
|
||||||
|
<ArrowUpRight className="h-3 w-3" />
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const internalColumns: ExtendedColumnDef<InternalResourceRow>[] = [
|
const internalColumns: ExtendedColumnDef<InternalResourceRow>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
@@ -185,21 +240,11 @@ export default function ClientResourcesTable({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "siteName",
|
accessorKey: "siteNames",
|
||||||
friendlyName: t("site"),
|
friendlyName: t("site"),
|
||||||
header: () => <span className="p-3">{t("site")}</span>,
|
header: () => <span className="p-3">{t("site")}</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
return <SiteCell resourceRow={row.original} />;
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={`/${resourceRow.orgId}/settings/sites/${resourceRow.siteNiceId}`}
|
|
||||||
>
|
|
||||||
<Button variant="outline">
|
|
||||||
{resourceRow.siteName}
|
|
||||||
<ArrowUpRight className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -399,7 +444,7 @@ export default function ClientResourcesTable({
|
|||||||
onConfirm={async () =>
|
onConfirm={async () =>
|
||||||
deleteInternalResource(
|
deleteInternalResource(
|
||||||
selectedInternalResource!.id,
|
selectedInternalResource!.id,
|
||||||
selectedInternalResource!.siteId
|
selectedInternalResource!.siteIds[0]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
string={selectedInternalResource.name}
|
string={selectedInternalResource.name}
|
||||||
@@ -433,7 +478,11 @@ export default function ClientResourcesTable({
|
|||||||
<EditInternalResourceDialog
|
<EditInternalResourceDialog
|
||||||
open={isEditDialogOpen}
|
open={isEditDialogOpen}
|
||||||
setOpen={setIsEditDialogOpen}
|
setOpen={setIsEditDialogOpen}
|
||||||
resource={editingResource}
|
resource={{
|
||||||
|
...editingResource,
|
||||||
|
siteName: editingResource.siteNames[0] ?? "",
|
||||||
|
siteId: editingResource.siteIds[0]
|
||||||
|
}}
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
sites={sites}
|
sites={sites}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user