mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-15 15:19:51 +02:00
💄 create and prefetch certificates for redirects
This commit is contained in:
@@ -28,6 +28,7 @@ export type ListRedirectsResponse = PaginatedResponse<{
|
||||
resourceName: string | null;
|
||||
resourceNiceId: string | null;
|
||||
resourceFullDomain: string | null;
|
||||
resourceDomainId: string | null;
|
||||
domainId: string | null;
|
||||
baseDomain: string | null;
|
||||
}>;
|
||||
@@ -150,6 +151,7 @@ export async function listRedirects(
|
||||
resourceName: resources.name,
|
||||
resourceNiceId: resources.niceId,
|
||||
resourceFullDomain: resources.fullDomain,
|
||||
resourceDomainId: resources.domainId,
|
||||
domainId: redirects.domainId,
|
||||
baseDomain: domains.baseDomain
|
||||
})
|
||||
|
||||
@@ -2,6 +2,8 @@ import RedirectsTable from "@app/components/RedirectsTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { build } from "@server/build";
|
||||
import type { GetBatchedCertificateResponse } from "@server/routers/certificates/types";
|
||||
import type { ListRedirectsResponse } from "@server/routers/redirect";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { Metadata } from "next";
|
||||
@@ -42,6 +44,60 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) {
|
||||
// empty list on error
|
||||
}
|
||||
|
||||
const redirectRows = redirects.map((redirect) => ({
|
||||
redirectId: redirect.redirectId,
|
||||
niceId: redirect.niceId,
|
||||
name: redirect.name,
|
||||
subdomain: redirect.subdomain,
|
||||
destinationDomain: redirect.destinationDomain,
|
||||
pathMatchType: redirect.pathMatchType,
|
||||
matchPath: redirect.matchPath,
|
||||
rewritePath: redirect.rewritePath,
|
||||
rewritePathType: redirect.rewritePathType,
|
||||
permanent: redirect.permanent,
|
||||
enabled: redirect.enabled,
|
||||
resourceId: redirect.resourceId,
|
||||
resourceName: redirect.resourceName,
|
||||
resourceNiceId: redirect.resourceNiceId,
|
||||
resourceFullDomain: redirect.resourceFullDomain,
|
||||
resourceDomainId: redirect.resourceDomainId,
|
||||
domainId: redirect.domainId,
|
||||
baseDomain: redirect.baseDomain
|
||||
}));
|
||||
|
||||
// Prefetched in one batched call so the table doesn't fire a separate
|
||||
// certificate request per visible row once it mounts on the client.
|
||||
const certDomains = Array.from(
|
||||
new Set(
|
||||
redirectRows
|
||||
.map((r) => {
|
||||
const domainHost = r.baseDomain
|
||||
? [r.subdomain, r.baseDomain].filter(Boolean).join(".")
|
||||
: null;
|
||||
return r.resourceFullDomain ?? domainHost;
|
||||
})
|
||||
.filter((host): host is string => Boolean(host))
|
||||
)
|
||||
);
|
||||
|
||||
let initialCertificates: GetBatchedCertificateResponse | undefined;
|
||||
if (build !== "oss" && certDomains.length > 0) {
|
||||
try {
|
||||
const certSearchParams = new URLSearchParams(
|
||||
certDomains.map((domain) => ["domains", domain])
|
||||
);
|
||||
const certRes = await internal.get<
|
||||
AxiosResponse<GetBatchedCertificateResponse>
|
||||
>(
|
||||
`/org/${orgId}/batched-certificates?${certSearchParams.toString()}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
initialCertificates = certRes.data.data;
|
||||
} catch {
|
||||
// leave undefined so each row falls back to fetching its own
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
@@ -51,30 +107,13 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) {
|
||||
|
||||
<RedirectsTable
|
||||
orgId={orgId}
|
||||
redirects={redirects.map((redirect) => ({
|
||||
redirectId: redirect.redirectId,
|
||||
niceId: redirect.niceId,
|
||||
name: redirect.name,
|
||||
subdomain: redirect.subdomain,
|
||||
destinationDomain: redirect.destinationDomain,
|
||||
pathMatchType: redirect.pathMatchType,
|
||||
matchPath: redirect.matchPath,
|
||||
rewritePath: redirect.rewritePath,
|
||||
rewritePathType: redirect.rewritePathType,
|
||||
permanent: redirect.permanent,
|
||||
enabled: redirect.enabled,
|
||||
resourceId: redirect.resourceId,
|
||||
resourceName: redirect.resourceName,
|
||||
resourceNiceId: redirect.resourceNiceId,
|
||||
resourceFullDomain: redirect.resourceFullDomain,
|
||||
domainId: redirect.domainId,
|
||||
baseDomain: redirect.baseDomain
|
||||
}))}
|
||||
redirects={redirectRows}
|
||||
rowCount={pagination.total}
|
||||
pagination={{
|
||||
pageIndex: pagination.page - 1,
|
||||
pageSize: pagination.pageSize
|
||||
}}
|
||||
initialCertificates={initialCertificates}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
ControlledDataTable,
|
||||
type ExtendedColumnDef
|
||||
} from "@app/components/ui/controlled-data-table";
|
||||
import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertIndicator";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import type { GetBatchedCertificateResponse } from "@server/routers/certificates/types";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
ArrowRight,
|
||||
@@ -48,6 +50,7 @@ export type RedirectRow = {
|
||||
resourceName: string | null;
|
||||
resourceNiceId: string | null;
|
||||
resourceFullDomain: string | null;
|
||||
resourceDomainId: string | null;
|
||||
domainId: string | null;
|
||||
baseDomain: string | null;
|
||||
};
|
||||
@@ -57,13 +60,15 @@ type RedirectsTableProps = {
|
||||
orgId: string;
|
||||
pagination: PaginationState;
|
||||
rowCount: number;
|
||||
initialCertificates?: GetBatchedCertificateResponse;
|
||||
};
|
||||
|
||||
export default function RedirectsTable({
|
||||
redirects,
|
||||
orgId,
|
||||
pagination,
|
||||
rowCount
|
||||
rowCount,
|
||||
initialCertificates
|
||||
}: RedirectsTableProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
@@ -263,16 +268,33 @@ export default function RedirectsTable({
|
||||
.join(".")
|
||||
: null;
|
||||
const host = redirect.resourceFullDomain ?? domainHost;
|
||||
// The cert lives on whichever domain actually terminates
|
||||
// TLS: the resource's domain when attached to a resource,
|
||||
// otherwise the redirect's own domain.
|
||||
const certDomainId =
|
||||
redirect.resourceDomainId ?? redirect.domainId;
|
||||
|
||||
return (
|
||||
<code className="text-sm truncate">
|
||||
{host ?? ""}
|
||||
<span className="text-muted-foreground">
|
||||
{redirect.pathMatchType === "prefix"
|
||||
? withPrefixGlob(redirect.matchPath)
|
||||
: redirect.matchPath}
|
||||
</span>
|
||||
</code>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{certDomainId && host ? (
|
||||
<ResourceAccessCertIndicator
|
||||
orgId={orgId}
|
||||
domainId={certDomainId}
|
||||
fullDomain={host}
|
||||
initialCertValue={
|
||||
initialCertificates?.[host]
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<code className="text-sm truncate">
|
||||
{host ?? ""}
|
||||
<span className="text-muted-foreground">
|
||||
{redirect.pathMatchType === "prefix"
|
||||
? withPrefixGlob(redirect.matchPath)
|
||||
: redirect.matchPath}
|
||||
</span>
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -371,7 +393,7 @@ export default function RedirectsTable({
|
||||
)
|
||||
}
|
||||
],
|
||||
[orgId, t]
|
||||
[orgId, t, initialCertificates]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user