💄 create and prefetch certificates for redirects

This commit is contained in:
Fred KISSIE
2026-09-14 23:49:22 +02:00
parent fd7780528f
commit b58cf31856
3 changed files with 92 additions and 29 deletions
+2
View File
@@ -28,6 +28,7 @@ export type ListRedirectsResponse = PaginatedResponse<{
resourceName: string | null; resourceName: string | null;
resourceNiceId: string | null; resourceNiceId: string | null;
resourceFullDomain: string | null; resourceFullDomain: string | null;
resourceDomainId: string | null;
domainId: string | null; domainId: string | null;
baseDomain: string | null; baseDomain: string | null;
}>; }>;
@@ -150,6 +151,7 @@ export async function listRedirects(
resourceName: resources.name, resourceName: resources.name,
resourceNiceId: resources.niceId, resourceNiceId: resources.niceId,
resourceFullDomain: resources.fullDomain, resourceFullDomain: resources.fullDomain,
resourceDomainId: resources.domainId,
domainId: redirects.domainId, domainId: redirects.domainId,
baseDomain: domains.baseDomain baseDomain: domains.baseDomain
}) })
+58 -19
View File
@@ -2,6 +2,8 @@ import RedirectsTable from "@app/components/RedirectsTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { internal } from "@app/lib/api"; import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; 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 { ListRedirectsResponse } from "@server/routers/redirect";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import type { Metadata } from "next"; import type { Metadata } from "next";
@@ -42,6 +44,60 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) {
// empty list on error // 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 ( return (
<> <>
<SettingsSectionTitle <SettingsSectionTitle
@@ -51,30 +107,13 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) {
<RedirectsTable <RedirectsTable
orgId={orgId} orgId={orgId}
redirects={redirects.map((redirect) => ({ redirects={redirectRows}
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
}))}
rowCount={pagination.total} rowCount={pagination.total}
pagination={{ pagination={{
pageIndex: pagination.page - 1, pageIndex: pagination.page - 1,
pageSize: pagination.pageSize pageSize: pagination.pageSize
}} }}
initialCertificates={initialCertificates}
/> />
</> </>
); );
+32 -10
View File
@@ -14,10 +14,12 @@ import {
ControlledDataTable, ControlledDataTable,
type ExtendedColumnDef type ExtendedColumnDef
} from "@app/components/ui/controlled-data-table"; } from "@app/components/ui/controlled-data-table";
import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertIndicator";
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 { 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 type { GetBatchedCertificateResponse } from "@server/routers/certificates/types";
import type { PaginationState } from "@tanstack/react-table"; import type { PaginationState } from "@tanstack/react-table";
import { import {
ArrowRight, ArrowRight,
@@ -48,6 +50,7 @@ export type RedirectRow = {
resourceName: string | null; resourceName: string | null;
resourceNiceId: string | null; resourceNiceId: string | null;
resourceFullDomain: string | null; resourceFullDomain: string | null;
resourceDomainId: string | null;
domainId: string | null; domainId: string | null;
baseDomain: string | null; baseDomain: string | null;
}; };
@@ -57,13 +60,15 @@ type RedirectsTableProps = {
orgId: string; orgId: string;
pagination: PaginationState; pagination: PaginationState;
rowCount: number; rowCount: number;
initialCertificates?: GetBatchedCertificateResponse;
}; };
export default function RedirectsTable({ export default function RedirectsTable({
redirects, redirects,
orgId, orgId,
pagination, pagination,
rowCount rowCount,
initialCertificates
}: RedirectsTableProps) { }: RedirectsTableProps) {
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
@@ -263,16 +268,33 @@ export default function RedirectsTable({
.join(".") .join(".")
: null; : null;
const host = redirect.resourceFullDomain ?? domainHost; 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 ( return (
<code className="text-sm truncate"> <div className="flex items-center gap-2 min-w-0">
{host ?? ""} {certDomainId && host ? (
<span className="text-muted-foreground"> <ResourceAccessCertIndicator
{redirect.pathMatchType === "prefix" orgId={orgId}
? withPrefixGlob(redirect.matchPath) domainId={certDomainId}
: redirect.matchPath} fullDomain={host}
</span> initialCertValue={
</code> 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 ( return (