💄 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
}) })
+50 -11
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,16 +44,7 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) {
// empty list on error // empty list on error
} }
return ( const redirectRows = redirects.map((redirect) => ({
<>
<SettingsSectionTitle
title={t("redirectsTitle")}
description={t("redirectsDescription")}
/>
<RedirectsTable
orgId={orgId}
redirects={redirects.map((redirect) => ({
redirectId: redirect.redirectId, redirectId: redirect.redirectId,
niceId: redirect.niceId, niceId: redirect.niceId,
name: redirect.name, name: redirect.name,
@@ -67,14 +60,60 @@ export default async function RedirectIndexPage(props: RedirectIndexPageProps) {
resourceName: redirect.resourceName, resourceName: redirect.resourceName,
resourceNiceId: redirect.resourceNiceId, resourceNiceId: redirect.resourceNiceId,
resourceFullDomain: redirect.resourceFullDomain, resourceFullDomain: redirect.resourceFullDomain,
resourceDomainId: redirect.resourceDomainId,
domainId: redirect.domainId, domainId: redirect.domainId,
baseDomain: redirect.baseDomain 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
title={t("redirectsTitle")}
description={t("redirectsDescription")}
/>
<RedirectsTable
orgId={orgId}
redirects={redirectRows}
rowCount={pagination.total} rowCount={pagination.total}
pagination={{ pagination={{
pageIndex: pagination.page - 1, pageIndex: pagination.page - 1,
pageSize: pagination.pageSize pageSize: pagination.pageSize
}} }}
initialCertificates={initialCertificates}
/> />
</> </>
); );
+24 -2
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,8 +268,24 @@ 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 (
<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"> <code className="text-sm truncate">
{host ?? ""} {host ?? ""}
<span className="text-muted-foreground"> <span className="text-muted-foreground">
@@ -273,6 +294,7 @@ export default function RedirectsTable({
: redirect.matchPath} : redirect.matchPath}
</span> </span>
</code> </code>
</div>
); );
} }
}, },
@@ -371,7 +393,7 @@ export default function RedirectsTable({
) )
} }
], ],
[orgId, t] [orgId, t, initialCertificates]
); );
return ( return (