diff --git a/server/routers/redirect/listRedirects.ts b/server/routers/redirect/listRedirects.ts index d26609222..a6970e279 100644 --- a/server/routers/redirect/listRedirects.ts +++ b/server/routers/redirect/listRedirects.ts @@ -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 }) diff --git a/src/app/[orgId]/settings/redirects/page.tsx b/src/app/[orgId]/settings/redirects/page.tsx index 5837b438e..99c3e4f5a 100644 --- a/src/app/[orgId]/settings/redirects/page.tsx +++ b/src/app/[orgId]/settings/redirects/page.tsx @@ -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 + >( + `/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 ( <> ({ - 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} /> ); diff --git a/src/components/RedirectsTable.tsx b/src/components/RedirectsTable.tsx index 7bea19fd6..e0b2c2802 100644 --- a/src/components/RedirectsTable.tsx +++ b/src/components/RedirectsTable.tsx @@ -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 ( - - {host ?? ""} - - {redirect.pathMatchType === "prefix" - ? withPrefixGlob(redirect.matchPath) - : redirect.matchPath} - - +
+ {certDomainId && host ? ( + + ) : null} + + {host ?? ""} + + {redirect.pathMatchType === "prefix" + ? withPrefixGlob(redirect.matchPath) + : redirect.matchPath} + + +
); } }, @@ -371,7 +393,7 @@ export default function RedirectsTable({ ) } ], - [orgId, t] + [orgId, t, initialCertificates] ); return (