feat(sharing): serve a configured public resume at root (#3470)

This commit is contained in:
Amruth Pillai
2026-09-05 19:42:44 -07:00
committed by GitHub
parent 870388192e
commit 744eaa902e
24 changed files with 768 additions and 26 deletions
+4
View File
@@ -9,6 +9,10 @@ SERVER_PORT="3001"
# OpenGraph metadata, and absolute upload URLs.
APP_URL="http://localhost:3000"
# Optional: serve one already-public resume at /. Use the ID from /builder/<id>.
# Unset or blank keeps the marketing home. Restart after changes.
# ROOT_RESUME_ID=
# --- Database (PostgreSQL) ---
# PostgreSQL connection URL. In Docker Compose, the hostname is usually `postgres`;
# when running directly on your machine, `localhost` is typical.
+27 -1
View File
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
env: { APP_URL: "https://rxresu.me" },
env: { APP_URL: "https://rxresu.me", ROOT_RESUME_ID: undefined as string | undefined },
serveStatic: vi.fn((_options?: unknown) => vi.fn()),
getPublicResumeSocialMeta: vi.fn(),
}));
@@ -45,6 +45,7 @@ const staticOptions = mocks.serveStatic.mock.calls[0]?.[0] as StaticOptions | un
describe("web app fallback classification", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.env.ROOT_RESUME_ID = undefined;
vi.mocked(fs.readFile).mockResolvedValue("<html>app</html>");
mocks.getPublicResumeSocialMeta.mockResolvedValue(null);
});
@@ -289,3 +290,28 @@ describe("web app fallback classification", () => {
expect(await unknownResponse.text()).toBe("");
});
});
describe("configured root shell", () => {
it.each(["GET", "HEAD"])("serves no-store noindex headers for %s", async (method) => {
mocks.env.ROOT_RESUME_ID = "private-or-missing-id";
const response = await handleWebApp(new Request("https://attacker.example/", { method }));
expect(response.headers.get("X-Robots-Tag")).toBe("noindex, follow");
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
});
it("uses configured canonical root without leaking ID or marketing metadata", async () => {
mocks.env.ROOT_RESUME_ID = "private-or-missing-id";
vi.mocked(fs.readFile).mockResolvedValue(
'<html><head><title>Marketing title</title><meta name="description" content="Marketing copy."></head><body></body></html>',
);
const html = await (
await handleWebApp(
new Request("https://attacker.example/?id=other", {
headers: { host: "attacker.example", "x-forwarded-host": "evil.example" },
}),
)
).text();
expect(html).toContain('<link rel="canonical" href="https://rxresu.me/" data-root-resume-shell>');
expect(html).toContain('<meta name="robots" content="noindex, follow" data-root-resume-shell>');
expect(html).not.toMatch(/private-or-missing-id|attacker|evil|Marketing|application\/ld\+json|timelapse/);
});
});
+18
View File
@@ -258,6 +258,14 @@ export const serveWebDistStatic = serveStatic({
});
function getFallbackResponseHeaders(pathname: string) {
if (pathname === "/" && env.ROOT_RESUME_ID?.trim()) {
return {
"Content-Type": "text/html; charset=UTF-8",
"X-Robots-Tag": "noindex, follow",
"Cache-Control": "private, no-store",
...BASE_SECURITY_HEADERS,
};
}
if (pathname === "/" || indexableAppPaths.has(pathname)) {
return { "Content-Type": "text/html; charset=UTF-8", ...BASE_SECURITY_HEADERS };
}
@@ -299,6 +307,16 @@ export async function handleWebApp(request: Request) {
const html = await fs.readFile(indexHtmlPath, "utf-8");
const canonicalUrl = new URL("/", env.APP_URL).toString();
if (pathname === "/" && env.ROOT_RESUME_ID?.trim()) {
// Root configuration never discloses a target in the HTML shell. The public API
// gates data and browser metadata; shell requests must not count extra views.
const shell = html
.replace(/<title>[^<]*<\/title>/, "<title>Reactive Resume</title>")
.replace(/<meta\s+name="description"[^>]*>/, '<meta name="description" content="">');
const markup = `<link rel="canonical" href="${escapeAttribute(canonicalUrl)}" data-root-resume-shell><meta name="robots" content="noindex, follow" data-root-resume-shell>`;
return new Response(shell.replace("</head>", `${markup}</head>`), { headers });
}
if (pathname === "/") {
return new Response(html.replace("</head>", `${createRootSeoMarkup(canonicalUrl)}</head>`), { headers });
}
@@ -18,19 +18,18 @@ const formSchema = z.object({
password: z.string().min(6).max(64),
});
type Props = {
type ResumePasswordPageProps = {
username: string;
slug: string;
redirectPath: string;
};
export function ResumePasswordPage({ redirectPath }: Props) {
export function ResumePasswordPage({ username, slug, redirectPath }: ResumePasswordPageProps) {
const navigate = useNavigate();
const [showPassword, toggleShowPassword] = useToggle(false);
const { mutate: verifyPassword } = useMutation(orpc.resume.verifyPassword.mutationOptions());
const [username, slug] = redirectPath.split("/").slice(1) as [string, string];
if (!username || !slug) throw navigate({ to: "/" });
const form = useAppForm({
defaultValues: { password: "" },
validators: { onSubmit: formSchema },
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { resumePasswordSearchSchema } from "./resume-password-search";
describe("resume password continuation", () => {
it("keeps ordinary slug redirects compatible", () => {
expect(resumePasswordSearchSchema.parse({ redirect: "/owner/resume" })).toEqual({ redirect: "/owner/resume" });
});
it("accepts root return independently of the verification identity", () => {
expect(resumePasswordSearchSchema.parse({ redirect: "/owner/resume", returnTo: "/" })).toEqual({
redirect: "/owner/resume",
returnTo: "/",
});
});
it.each(["//evil.example", "https://evil.example", "/\\evil.example", "/other/path", "/?next=evil"])(
"rejects return path %s",
(returnTo) => {
expect(resumePasswordSearchSchema.safeParse({ redirect: "/owner/resume", returnTo }).success).toBe(false);
},
);
it.each([
"/",
"//evil.example",
"/owner/slug/extra",
"/owner/slug?next=evil",
"/owner/%2f%2fevil",
"/owner/\\evil",
"/owner/slug#hash",
])("rejects malformed verification identity %s", (redirect) => {
expect(resumePasswordSearchSchema.safeParse({ redirect, returnTo: "/" }).success).toBe(false);
});
});
@@ -0,0 +1,8 @@
import z from "zod";
// Keep the existing slug-shaped `redirect` as verification identity. The optional
// continuation is deliberately limited to the configured instance root.
export const resumePasswordSearchSchema = z.object({
redirect: z.string().regex(/^\/[^/\\?#%\s]+\/[^/\\?#%\s]+$/),
returnTo: z.literal("/").optional(),
});
@@ -130,3 +130,26 @@ describe("PublicResumeRoute", () => {
expect(viewerFrame).not.toHaveClass("min-h-0", "flex-1", "overflow-hidden");
});
});
describe("PublicResumePage at root", () => {
it("renders supplied identity and links to dashboard without slug route hooks", async () => {
const { PublicResumePage } = await import("./public-resume");
render(
<I18nProvider i18n={i18n}>
<PublicResumePage
resume={publicResumeMock.resume}
username="root-owner"
slug="renamed"
flags={publicResumeMock.flags}
isRoot
/>
</I18nProvider>,
);
expect(screen.getByRole("link", { name: /Build your own resume/ })).toHaveAttribute("href", "/dashboard");
expect(screen.getByRole("main")).toHaveAttribute("id", "main-content");
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(sampleResumeData.basics.name);
expect(publicResumeMock.useResumeExport).toHaveBeenCalledWith(publicResumeMock.resume, {
publicResumePdf: { publicResume: { username: "root-owner", slug: "renamed" } },
});
});
});
@@ -1,3 +1,4 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { CircleNotchIcon, DownloadSimpleIcon } from "@phosphor-icons/react";
@@ -18,6 +19,18 @@ export function PublicResumeRoute() {
const { flags } = publicResumeRoute.useRouteContext();
const { data: resume } = useQuery(orpc.resume.getBySlug.queryOptions({ input: { username, slug } }));
return <PublicResumePage resume={resume} username={username} slug={slug} flags={flags} />;
}
type PublicResumePageProps = {
resume: { id?: string; name: string; slug: string; data: ResumeData; showDownloadButtons?: boolean } | undefined;
username: string;
slug: string;
flags: { disableSignups: boolean };
isRoot?: boolean;
};
export function PublicResumePage({ resume, username, slug, flags, isRoot = false }: PublicResumePageProps) {
const publicResume = useMemo(() => ({ username, slug }), [slug, username]);
const { onDownloadPDF, isExporting } = useResumeExport(resume, {
...(resume ? { publicResumePdf: { publicResume } } : {}),
@@ -51,14 +64,14 @@ export function PublicResumeRoute() {
)}
</header>
<main className="w-full max-w-5xl bg-white print:max-w-full">
<main id="main-content" className="w-full max-w-5xl bg-white print:max-w-full">
<PdfViewer data={resume.data} className="block w-full" publicResume={publicResume} />
</main>
{!flags.disableSignups && (
<footer className="flex justify-center print:hidden">
<a
href="/"
href={isRoot ? "/dashboard" : "/"}
className="flex items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground"
>
<BrandIcon variant="icon" className="size-5" />
+13
View File
@@ -8,6 +8,19 @@ if (!rootElement) throw new Error("Root element not found");
const router = await getRouter();
// Server metadata describes the initial URL. The SPA router owns these tags after
// startup, including navigation into root mode from another marketing/public page.
const serverSeoSelectors = [
"[data-root-resume-shell]",
'head link[rel="canonical"]',
'head script[type="application/ld+json"]',
'head meta[property^="og:"]',
'head meta[name^="twitter:"]',
];
document.querySelectorAll(serverSeoSelectors.join(",")).forEach((element) => {
element.remove();
});
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
+1
View File
@@ -9,6 +9,7 @@ import { createNoindexFollowMeta, createResumeSocialMeta, getCanonicalRootUrl }
type LoaderData = Omit<RouterOutput["resume"]["getBySlug"], "data"> & { data: ResumeData };
export const Route = createFileRoute("/$username/$slug")({
ssr: "data-only",
component: lazyRouteComponent(() => import("@/features/resume/public/public-resume"), "PublicResumeRoute"),
loader: async ({ context, params }) => {
const { username, slug } = params;
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it, vi } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
vi.mock("@tanstack/react-router", () => ({
createFileRoute: () => (options: unknown) => ({ options }),
lazyRouteComponent: () => () => null,
redirect: (options: unknown) => options,
}));
vi.mock("@/libs/orpc/client", () => ({
orpc: { resume: { getRoot: { queryOptions: () => ({ queryKey: ["root"] }) } } },
}));
// Marketing sections are unrelated to the loader/head boundary under test.
vi.mock("./-sections/donate", () => ({ DonationBanner: () => null }));
vi.mock("./-sections/faq", () => ({ Faq: () => null }));
vi.mock("./-sections/features", () => ({ Features: () => null }));
vi.mock("./-sections/footer", () => ({ Footer: () => null }));
vi.mock("./-sections/hero", () => ({ Hero: () => null }));
vi.mock("./-sections/prefooter", () => ({ Prefooter: () => null }));
vi.mock("./-sections/sponsors", () => ({ Sponsors: () => null }));
vi.mock("./-sections/statistics", () => ({ Statistics: () => null }));
vi.mock("./-sections/templates", () => ({ Templates: () => null }));
vi.mock("./-sections/testimonials", () => ({ Testimonials: () => null }));
const { Route } = await import("./index");
describe("home root mode", () => {
it("uses server canonical root for public metadata", async () => {
const head = await Route.options.head?.({
loaderData: {
root: {
status: "public",
canonicalUrl: "https://configured.example/",
username: "owner",
slug: "resume",
resume: { data: defaultResumeData, name: "Root Fixture" },
},
},
} as never);
expect(head).toMatchObject({ links: [{ rel: "canonical", href: "https://configured.example/" }] });
expect(head?.meta).toContainEqual({ name: "robots", content: "noindex, follow" });
expect(head?.scripts).toBeUndefined();
});
it("keeps unavailable metadata free from target details and marketing structured data", async () => {
const head = await Route.options.head?.({
loaderData: { root: { status: "unavailable", canonicalUrl: "https://configured.example/" } },
} as never);
expect(head?.meta).toContainEqual({ name: "robots", content: "noindex, follow" });
expect(head?.scripts).toBeUndefined();
});
it("retains marketing structured data when disabled", async () => {
const head = await Route.options.head?.({ loaderData: { root: { status: "disabled" } } } as never);
expect(head?.scripts).toHaveLength(1);
});
});
+62 -3
View File
@@ -1,5 +1,15 @@
import { createFileRoute } from "@tanstack/react-router";
import { createRootStructuredDataScript, getCanonicalRootUrl } from "@/libs/seo";
import { ORPCError } from "@orpc/client";
import { ClientOnly, createFileRoute, lazyRouteComponent, redirect } from "@tanstack/react-router";
import { getResumeSocialMeta } from "@reactive-resume/resume/social-meta";
import { LoadingScreen } from "@/components/layout/loading-screen";
import { NotFoundScreen } from "@/components/layout/not-found-screen";
import { orpc } from "@/libs/orpc/client";
import {
createNoindexFollowMeta,
createResumeSocialMeta,
createRootStructuredDataScript,
getCanonicalRootUrl,
} from "@/libs/seo";
import { DonationBanner } from "./-sections/donate";
import { Faq } from "./-sections/faq";
import { Features } from "./-sections/features";
@@ -11,9 +21,44 @@ import { Statistics } from "./-sections/statistics";
import { Templates } from "./-sections/templates";
import { Testimonials } from "./-sections/testimonials";
const PublicResumePage = lazyRouteComponent(() => import("@/features/resume/public/public-resume"), "PublicResumePage");
export const Route = createFileRoute("/_home/")({
component: RouteComponent,
head: () => {
loader: async ({ context }) => ({
root: await context.queryClient.fetchQuery(orpc.resume.getRoot.queryOptions({ staleTime: 0 })),
}),
onError: (error) => {
if (error instanceof ORPCError && error.code === "NEED_PASSWORD") {
const { username, slug } = error.data as { username: string; slug: string };
throw redirect({ to: "/auth/resume-password", search: { redirect: `/${username}/${slug}`, returnTo: "/" } });
}
},
head: ({ loaderData }) => {
const root = loaderData?.root;
if (root && root.status !== "disabled") {
const { canonicalUrl } = root;
if (root.status === "unavailable") {
return {
meta: [{ title: "Reactive Resume" }, createNoindexFollowMeta()],
links: [{ rel: "canonical", href: canonicalUrl }],
};
}
const social = getResumeSocialMeta(root.resume.data, root.resume.name || "Resume");
return {
meta: [
{ title: `${social.name} - Reactive Resume` },
createNoindexFollowMeta(),
...createResumeSocialMeta({
canonicalUrl,
title: social.title,
description: social.description,
imageUrl: `${canonicalUrl}opengraph/banner.jpg`,
}),
],
links: [{ rel: "canonical", href: canonicalUrl }],
};
}
const appUrl = typeof window !== "undefined" ? window.location.origin : "https://rxresu.me";
const canonicalUrl = getCanonicalRootUrl(appUrl);
@@ -29,6 +74,20 @@ export const Route = createFileRoute("/_home/")({
function RouteComponent() {
const { flags } = Route.useRouteContext();
const { root } = Route.useLoaderData();
if (root.status === "unavailable")
return (
<main id="main-content">
<NotFoundScreen />
</main>
);
if (root.status === "public") {
return (
<ClientOnly fallback={<LoadingScreen />}>
<PublicResumePage resume={root.resume} username={root.username} slug={root.slug} flags={flags} isRoot />
</ClientOnly>
);
}
return (
<main id="main-content" className="relative">
+5 -2
View File
@@ -1,5 +1,5 @@
import { Trans } from "@lingui/react/macro";
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { createFileRoute, Outlet, useMatch } from "@tanstack/react-router";
import { Header } from "./-sections/header";
export const Route = createFileRoute("/_home")({
@@ -7,6 +7,9 @@ export const Route = createFileRoute("/_home")({
});
function RouteComponent() {
const rootMatch = useMatch({ from: "/_home/", shouldThrow: false });
const rootMode = rootMatch?.loaderData?.root.status;
const showMarketingHeader = !rootMode || rootMode === "disabled";
return (
<>
<a
@@ -16,7 +19,7 @@ function RouteComponent() {
<Trans>Skip to main content</Trans>
</a>
<Header />
{showMarketingHeader && <Header />}
<Outlet />
</>
);
+5 -11
View File
@@ -1,17 +1,10 @@
import { createFileRoute, redirect, SearchParamError } from "@tanstack/react-router";
import z from "zod";
import { ResumePasswordPage } from "@/features/auth/pages/resume-password";
const searchSchema = z.object({
redirect: z
.string()
.min(1)
.regex(/^\/[^/]+\/[^/]+$/),
});
import { resumePasswordSearchSchema } from "@/features/auth/resume-password-search";
export const Route = createFileRoute("/auth/resume-password")({
component: RouteComponent,
validateSearch: searchSchema,
validateSearch: resumePasswordSearchSchema,
onError: (error) => {
if (error instanceof SearchParamError) {
throw redirect({ to: "/" });
@@ -20,7 +13,8 @@ export const Route = createFileRoute("/auth/resume-password")({
});
function RouteComponent() {
const { redirect } = Route.useSearch();
const { redirect, returnTo } = Route.useSearch();
const [username, slug] = redirect.slice(1).split("/") as [string, string];
return <ResumePasswordPage redirectPath={redirect} />;
return <ResumePasswordPage username={username} slug={slug} redirectPath={returnTo ?? redirect} />;
}
+18
View File
@@ -574,3 +574,21 @@ A healthy response returns HTTP 200. If you get a different status code, the JSO
- **Fix**: Set `S3_FORCE_PATH_STYLE="true"` in your environment. This is required for most self-hosted S3-compatible services like MinIO, SeaweedFS, etc.
</Accordion>
</AccordionGroup>
## Serve a public resume at the instance root
To display one public resume at `/` instead of the marketing home, set the optional server environment variable `ROOT_RESUME_ID` on the application service:
```yaml
environment:
APP_URL: https://resume.example.com
ROOT_RESUME_ID: your-resume-id
```
Find the resume ID in its owner's builder URL: `/builder/<resume-id>`. The resume must already have **Allow Public Access** enabled in Sharing. This setting does not change its visibility. Password protection and the download-button preference still apply, and the ordinary `/<username>/<slug>` URL continues to work. Renaming the username or slug does not change the configured ID.
Restart the application after setting or changing `ROOT_RESUME_ID`. With Docker Compose, run `docker compose up -d` to recreate the application with the new environment. Unset the variable or leave it blank, then restart, to restore the marketing home. A missing, deleted, or private target shows an unavailable page, including when its owner visits `/`.
Keep `APP_URL` set to the public origin and proxy the whole application normally, including API, uploads, fonts, and assets. Root mode uses that configured origin for its canonical URL; it does not infer a domain from request headers. A successful password challenge returns visitors to `/`.
This is a single-resume setting for one self-hosted instance. It does not register custom domains, manage DNS or TLS, or hide the rest of the application. Login and the dashboard remain available at their usual paths.
@@ -0,0 +1,97 @@
import { describe, expect, it, vi } from "vitest";
import { ORPCError } from "@orpc/server";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { getRootResume } from "./root";
const fixture = () => ({
config: { rootResumeId: "root-id", appUrl: "https://resume.example/base?ignored=yes" },
findTarget: vi.fn(async (_id: string) => ({ username: "owner", slug: "current-slug", isPublic: true })),
getBySlug: vi.fn(async (_input: unknown) => ({
id: "root-id",
name: "Resume",
slug: "current-slug",
data: defaultResumeData,
tags: [],
isPublic: true,
isLocked: false,
showDownloadButtons: false,
hasPassword: false,
})),
});
const request = { requestHeaders: new Headers({ host: "attacker.example", "x-forwarded-host": "attacker.example" }) };
describe("configured root public resume", () => {
it.each([undefined, "", " "])("disables root mode for %s without a lookup", async (rootResumeId) => {
const deps = fixture();
const result = await getRootResume(request, { ...deps, config: { ...deps.config, rootResumeId } });
expect(result).toEqual({ status: "disabled" });
expect(deps.findTarget).not.toHaveBeenCalled();
expect(deps.getBySlug).not.toHaveBeenCalled();
});
it.each([null, { username: "secret-owner", slug: "secret-slug", isPublic: false }])(
"does not disclose an unavailable target to its owner",
async (target) => {
const deps = fixture();
const result = await getRootResume(
{ ...request, currentUserId: "owner-id" },
{ ...deps, findTarget: async () => target },
);
expect(result).toEqual({ status: "unavailable", canonicalUrl: "https://resume.example/" });
expect(deps.getBySlug).not.toHaveBeenCalled();
},
);
it("resolves only configured ID and delegates once with public-only enforcement", async () => {
const deps = fixture();
const result = await getRootResume({ ...request, currentUserId: "owner-id" }, deps);
expect(result).toMatchObject({
status: "public",
username: "owner",
slug: "current-slug",
canonicalUrl: "https://resume.example/",
resume: { showDownloadButtons: false, hasPassword: false },
});
expect(deps.findTarget).toHaveBeenCalledExactlyOnceWith("root-id");
expect(deps.getBySlug).toHaveBeenCalledExactlyOnceWith({
...request,
currentUserId: "owner-id",
username: "owner",
slug: "current-slug",
requirePublic: true,
expectedResumeId: "root-id",
});
});
it("uses the renamed slug on the next request", async () => {
const deps = fixture();
deps.findTarget.mockResolvedValue({ username: "renamed-owner", slug: "renamed-slug", isPublic: true });
expect(await getRootResume(request, deps)).toMatchObject({ username: "renamed-owner", slug: "renamed-slug" });
});
it("keeps a privacy change during the final lookup unavailable", async () => {
const deps = fixture();
deps.getBySlug.mockRejectedValue(new ORPCError("NOT_FOUND"));
expect(await getRootResume(request, deps)).toEqual({
status: "unavailable",
canonicalUrl: "https://resume.example/",
});
});
it("preserves the existing password challenge identity", async () => {
const deps = fixture();
deps.getBySlug.mockRejectedValue(
new ORPCError("NEED_PASSWORD", { status: 401, data: { username: "owner", slug: "current-slug" } }),
);
await expect(getRootResume(request, deps)).rejects.toMatchObject({
code: "NEED_PASSWORD",
data: { username: "owner", slug: "current-slug" },
});
});
it("does not hide infrastructure failures as missing resumes", async () => {
const deps = fixture();
deps.getBySlug.mockRejectedValue(new Error("database unavailable"));
await expect(getRootResume(request, deps)).rejects.toThrow("database unavailable");
});
});
+57
View File
@@ -0,0 +1,57 @@
import type { resumeService } from "./service";
import { ORPCError } from "@orpc/server";
type RootRequest = { requestHeaders: Headers; currentUserId?: string };
type RootDependencies = {
config: { rootResumeId?: string | undefined; appUrl: string };
findTarget(id: string): Promise<{ username: string; slug: string; isPublic: boolean } | null>;
getBySlug: typeof resumeService.getBySlug;
};
const getDependencies = async (): Promise<RootDependencies> => {
const [{ env }, { db }, schema, { eq }, { resumeService }] = await Promise.all([
import("@reactive-resume/env/server"),
import("@reactive-resume/db/client"),
import("@reactive-resume/db/schema"),
import("drizzle-orm"),
import("./service"),
]);
return {
config: { rootResumeId: env.ROOT_RESUME_ID, appUrl: env.APP_URL },
findTarget: async (id) => {
const [target] = await db
.select({ username: schema.user.username, slug: schema.resume.slug, isPublic: schema.resume.isPublic })
.from(schema.resume)
.innerJoin(schema.user, eq(schema.resume.userId, schema.user.id))
.where(eq(schema.resume.id, id));
return target ?? null;
},
getBySlug: resumeService.getBySlug,
};
};
/** Instance configuration is the sole authority; callers cannot choose a host or target. */
export async function getRootResume(input: RootRequest, dependencies?: RootDependencies) {
const { config, findTarget, getBySlug } = dependencies ?? (await getDependencies());
const id = config.rootResumeId?.trim();
if (!id) return { status: "disabled" as const };
const canonicalUrl = new URL("/", config.appUrl).href;
const unavailable = { status: "unavailable" as const, canonicalUrl };
const target = await findTarget(id);
if (!target?.isPublic) return unavailable;
try {
const resume = await getBySlug({
...input,
username: target.username,
slug: target.slug,
requirePublic: true,
expectedResumeId: id,
});
return { status: "public" as const, canonicalUrl, username: target.username, slug: target.slug, resume };
} catch (error) {
if (error instanceof ORPCError && error.code === "NOT_FOUND") return unavailable;
throw error;
}
}
@@ -1,11 +1,19 @@
import { publicProcedure } from "../../context";
import { crudRouter } from "./crud";
import { updatesRouter } from "./event-router";
import { getRootResume } from "./root";
import { sharingRouter } from "./sharing";
import { resumeStatisticsRouter } from "./statistics";
import { tagsRouter } from "./tags";
import { versionsRouter } from "./versions";
export const resumeRouter = {
getRoot: publicProcedure.handler(({ context }) =>
getRootResume({
requestHeaders: context.reqHeaders,
...(context.user?.id ? { currentUserId: context.user.id } : {}),
}),
),
tags: tagsRouter,
statistics: resumeStatisticsRouter,
updates: updatesRouter,
@@ -1096,3 +1096,47 @@ describe("statistics.recordDownload", () => {
expect(values).toHaveBeenCalledTimes(2);
});
});
describe("root public-only lookup", () => {
it("rejects a private target even for its owner after identity resolution", async () => {
const row = {
...createResumeRow(defaultResumeData),
userId: "u1",
isPublic: false,
hasPassword: false,
passwordHash: null,
};
dbMock.select.mockReturnValue({ from: () => ({ innerJoin: () => ({ where: async () => [row] }) }) });
await expect(
resumeService.getBySlug({
username: "owner",
slug: "resume",
requestHeaders: new Headers(),
currentUserId: "u1",
requirePublic: true,
}),
).rejects.toMatchObject({ code: "NOT_FOUND" });
});
});
it("rejects a different resume reusing the resolved root slug", async () => {
const row = {
...createResumeRow(defaultResumeData),
id: "replacement-id",
userId: "u1",
isPublic: true,
hasPassword: false,
passwordHash: null,
};
dbMock.select.mockReturnValue({ from: () => ({ innerJoin: () => ({ where: async () => [row] }) }) });
await expect(
resumeService.getBySlug({
username: "owner",
slug: "resume",
requestHeaders: new Headers(),
currentUserId: "u1",
requirePublic: true,
expectedResumeId: "configured-id",
}),
).rejects.toMatchObject({ code: "NOT_FOUND" });
});
+14 -2
View File
@@ -512,7 +512,14 @@ export const resumeService = {
return resume;
},
getBySlug: async (input: { username: string; slug: string; requestHeaders: Headers; currentUserId?: string }) => {
getBySlug: async (input: {
username: string;
slug: string;
requestHeaders: Headers;
currentUserId?: string;
requirePublic?: boolean;
expectedResumeId?: string;
}) => {
const [resume] = await db
.select({
id: schema.resume.id,
@@ -531,7 +538,12 @@ export const resumeService = {
.innerJoin(schema.user, eq(schema.resume.userId, schema.user.id))
.where(and(eq(schema.resume.slug, input.slug), eq(schema.user.username, input.username)));
if (!resume) throw new ORPCError("NOT_FOUND");
if (
!resume ||
(input.requirePublic && !resume.isPublic) ||
(input.expectedResumeId && resume.id !== input.expectedResumeId)
)
throw new ORPCError("NOT_FOUND");
const viewer = input.currentUserId ? { id: input.currentUserId } : null;
assertCanView(resume, viewer);
+24
View File
@@ -0,0 +1,24 @@
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("@reactive-resume/utils/monorepo.node", () => ({ findWorkspaceRoot: () => undefined }));
afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
});
describe("root resume configuration", () => {
it.each([
[undefined, undefined],
["", undefined],
[" ", undefined],
[" root-id ", "root-id"],
])("normalizes %s to %s", async (value, expected) => {
vi.stubEnv("APP_URL", "https://resume.example");
vi.stubEnv("DATABASE_URL", "postgresql://localhost/disposable");
vi.stubEnv("AUTH_SECRET", "disposable");
vi.stubEnv("ROOT_RESUME_ID", value);
const { env } = await import("./server");
expect(env.ROOT_RESUME_ID).toBe(expected);
});
});
+5
View File
@@ -19,6 +19,11 @@ export const env = createEnv({
server: {
// Application
APP_URL: z.url({ protocol: /https?/ }),
ROOT_RESUME_ID: z
.string()
.trim()
.transform((value) => value || undefined)
.optional(),
SERVER_PORT: z.coerce.number().int().min(1).max(65535).default(3001),
// Database
+231
View File
@@ -0,0 +1,231 @@
import type { BrowserContext } from "@playwright/test";
import { readFile } from "node:fs/promises";
import { Pool } from "pg";
import { createAuthenticatedContext } from "../fixtures/auth";
import { createAccount } from "../fixtures/data";
import { deleteE2EUser } from "../fixtures/db";
import { expect, test } from "../fixtures/test";
const rootId = process.env.ROOT_RESUME_ID?.trim();
test("unset root mode retains marketing and ordinary app entry points", async ({ page, request }) => {
test.skip(Boolean(rootId), "Run once with ROOT_RESUME_ID unset, then restart with an e2e- ID.");
await page.goto("/");
await expect(page.getByRole("main")).toHaveAttribute("id", "main-content");
await expect(page.locator('script[type="application/ld+json"]')).not.toHaveCount(0);
expect((await request.get("/api/health")).ok()).toBe(true);
await page.goto("/auth/login");
await expect(page.getByRole("button", { name: "Sign in", exact: true })).toBeVisible();
});
test("configured root preserves access, canonical, identity, statistics and app routes", async ({
browser,
authPage: owner,
account,
baseURL,
}, testInfo) => {
test.skip(!rootId, "Requires a disposable database and ROOT_RESUME_ID=e2e-root-2669.");
test.setTimeout(120_000);
if (!rootId?.startsWith("e2e-")) throw new Error("ROOT_RESUME_ID must begin with e2e- for this disposable fixture.");
if (!baseURL) throw new Error("APP_URL is required for root E2E.");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const visitor = await browser.newContext({ baseURL, userAgent: "root-resume-e2e-visitor" });
const page = await visitor.newPage();
const failures: string[] = [];
const fontStatuses: number[] = [];
const assetStatuses: number[] = [];
page.on("response", (response) => {
if (/\.(woff2?|ttf|otf)(?:\?|$)/.test(response.url())) fontStatuses.push(response.status());
if (new URL(response.url()).pathname.startsWith("/assets/")) assetStatuses.push(response.status());
});
const secondAccount = createAccount(testInfo);
let other: BrowserContext | undefined;
const otherBootstrap = await browser.newContext({ baseURL });
page.on("pageerror", (error) => failures.push(error.message));
const unavailable = async () => {
await page.goto("/");
await expect(page.getByText("We couldn't find that page", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Download PDF" })).toHaveCount(0);
await expect(page.locator("body")).not.toContainText("Root Fixture");
await expect(page.locator("body")).not.toContainText(rootId);
await expect(page.getByRole("main")).toHaveCount(1);
await expect(page.getByRole("navigation", { name: "Main navigation" })).toHaveCount(0);
};
try {
await unavailable();
other = await createAuthenticatedContext(browser, otherBootstrap.request, secondAccount, baseURL);
const otherCreated = await other.request.post("/api/openapi/resumes", {
data: { name: "Other public fixture", slug: "other-public", tags: [], withSampleData: true },
});
expect(otherCreated.ok()).toBe(true);
const otherResumeId = await otherCreated.json();
expect((await other.request.put(`/api/openapi/resumes/${otherResumeId}`, { data: { isPublic: true } })).ok()).toBe(
true,
);
const created = await owner.request.post("/api/openapi/resumes", {
data: { name: "Root Fixture", slug: "root-fixture", tags: [], withSampleData: true },
});
expect(created.ok()).toBe(true);
const originalId = await created.json();
await pool.query(
'update resume set id = $1 where id = $2 and user_id = (select id from "user" where username = $3)',
[rootId, originalId, account.username],
);
await pool.query(`update resume set data = jsonb_set(data, '{basics,name}', '"Root Fixture"') where id = $1`, [
rootId,
]);
const resource = `/api/openapi/resumes/${rootId}`;
const update = async (data: Record<string, unknown>) => {
const response = await owner.request.put(resource, { data });
expect(response.ok()).toBe(true);
};
const readStatistics = async () => (await owner.request.get(`${resource}/statistics`)).json();
await unavailable();
await owner.goto("/");
await expect(owner.getByText("We couldn't find that page", { exact: true })).toBeVisible();
const otherRoot = await other.request.post("/api/rpc/resume/getRoot", { data: { json: null } });
expect(await otherRoot.json()).toEqual({ json: { status: "unavailable", canonicalUrl: `${baseURL}/` } });
await owner.setViewportSize({ width: 1920, height: 950 });
await owner.goto(`/builder/${rootId}`);
await expect(owner.locator("#sidebar-picture")).toBeVisible();
await owner.getByRole("button", { name: "Contain", exact: true }).click();
const pictureData = await owner.evaluate(() => {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 64;
const context = canvas.getContext("2d");
if (!context) throw new Error("Canvas unavailable");
context.fillStyle = "#4488aa";
context.fillRect(0, 0, 64, 64);
return canvas.toDataURL("image/png").slice("data:image/png;base64,".length);
});
await owner
.locator('#sidebar-picture input[type="file"]')
.setInputFiles({ name: "root-fixture.png", mimeType: "image/png", buffer: Buffer.from(pictureData, "base64") });
const pictureInput = owner.locator('#sidebar-picture input[name="url"]');
await expect(pictureInput).toHaveValue(/\/uploads\//);
const pictureUrl = await pictureInput.inputValue();
const pictureResponse = await visitor.request.get(pictureUrl);
expect(pictureResponse.ok()).toBe(true);
expect(pictureResponse.headers()["content-type"]).toMatch(/^image\//);
await owner.goto("/");
await expect(owner.getByText("We couldn't find that page", { exact: true })).toBeVisible();
await update({ isPublic: true });
await owner.reload();
await expect(owner.getByRole("button", { name: "Download PDF" }).first()).toBeVisible();
expect(await readStatistics()).toMatchObject({ views: 0, downloads: 0 });
await page.reload();
await expect(page.getByRole("button", { name: "Download PDF" }).first()).toBeVisible();
await expect(page.locator("canvas").first()).toBeVisible();
await expect(page.getByRole("heading", { name: "Root Fixture", exact: true })).toBeVisible();
await page.screenshot({ path: testInfo.outputPath("root-public.png"), fullPage: true });
await expect(page.locator("header img")).toHaveAttribute("src", pictureUrl);
await expect(page).toHaveURL(`${baseURL}/`);
await expect(page.getByRole("main")).toHaveCount(1);
await expect(page.getByRole("navigation", { name: "Main navigation" })).toHaveCount(0);
await expect(page.getByRole("link", { name: "Build your own resume" })).toHaveAttribute("href", "/dashboard");
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute("href", `${baseURL}/`);
await expect(page.locator('script[type="application/ld+json"]')).toHaveCount(0);
await expect.poll(readStatistics).toMatchObject({ views: 1, downloads: 0 });
await page.reload();
await expect(page.getByRole("button", { name: "Download PDF" }).first()).toBeVisible();
expect(await readStatistics()).toMatchObject({ views: 1, downloads: 0 });
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Download PDF" }).first().click();
const download = await downloadPromise;
expect(await download.failure()).toBeNull();
const downloadPath = await download.path();
if (!downloadPath) throw new Error("Missing root PDF download");
expect((await readFile(downloadPath)).subarray(0, 5).toString()).toBe("%PDF-");
await expect.poll(readStatistics).toMatchObject({ views: 1, downloads: 1 });
await update({ slug: "renamed-root", showDownloadButtons: false });
await page.reload();
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
await expect(page.getByRole("button", { name: "Download PDF" })).toHaveCount(0);
await expect(page).toHaveURL(`${baseURL}/`);
const slugPath = `/${account.username}/renamed-root`;
await page.goto(slugPath);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
await expect(page.locator('link[rel="canonical"]').last()).toHaveAttribute("href", `${baseURL}${slugPath}`);
await update({ showDownloadButtons: true });
await page.goto("/ats-checker");
await page.getByRole("link", { name: "Reactive Resume - Go to homepage", exact: true }).click();
await expect(page.getByRole("heading", { name: "Root Fixture", exact: true })).toBeVisible();
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute("href", `${baseURL}/`);
await expect(page.locator('script[type="application/ld+json"]')).toHaveCount(0);
const password = "root-e2e-secret";
expect((await owner.request.put(`${resource}/password`, { data: { password } })).ok()).toBe(true);
await page.goto("/");
await expect(page).toHaveURL(/\/auth\/resume-password/);
expect(new URL(page.url()).searchParams.get("returnTo")).toBe("/");
await page.getByLabel("Password", { exact: true }).fill("wrong-password");
await page.getByRole("button", { name: "Unlock", exact: true }).click();
await expect(page.getByText("The password you entered is incorrect", { exact: true })).toBeVisible();
const lockedPdf = await visitor.request.get(`/api/resumes/${account.username}/renamed-root/pdf`);
expect(lockedPdf.status()).toBe(401);
await page.getByLabel("Password", { exact: true }).fill(password);
await page.getByRole("button", { name: "Unlock", exact: true }).click();
await expect(page).toHaveURL(`${baseURL}/`);
await expect(page.getByRole("button", { name: "Download PDF" }).first()).toBeVisible();
await page.reload();
await expect(page).toHaveURL(`${baseURL}/`);
await expect(page.getByRole("button", { name: "Download PDF" }).first()).toBeVisible();
const accessCookies = (await visitor.cookies()).filter((cookie) => cookie.name.startsWith("resume_access_"));
expect(accessCookies.length).toBeGreaterThan(0);
await visitor.addCookies(
accessCookies.map((cookie) => ({ ...cookie, expires: Math.floor(Date.now() / 1000) - 1 })),
);
await page.reload();
await expect(page).toHaveURL(/\/auth\/resume-password/);
expect((await visitor.request.get(`/api/resumes/${account.username}/renamed-root/pdf`)).status()).toBe(401);
await update({ isPublic: false });
await unavailable();
await owner.goto("/");
await expect(owner.getByText("We couldn't find that page", { exact: true })).toBeVisible();
const rootResponse = await visitor.request.post("/api/rpc/resume/getRoot", {
data: {
json: {
id: otherResumeId,
username: secondAccount.username,
slug: "other-public",
host: "evil.example",
requirePublic: false,
},
},
headers: { "x-forwarded-host": "evil.example" },
});
expect(rootResponse.ok()).toBe(true);
expect(await rootResponse.json()).toEqual({ json: { status: "unavailable", canonicalUrl: `${baseURL}/` } });
const shell = await visitor.request.get("/", {
headers: { host: "attacker.example", "x-forwarded-host": "evil.example" },
});
expect(await shell.text()).toContain(`<link rel="canonical" href="${baseURL}/" data-root-resume-shell>`);
expect(shell.headers()["x-robots-tag"]).toBe("noindex, follow");
for (const path of ["/api/health", "/auth/login", "/favicon.svg"]) {
const response = await visitor.request.get(path);
expect(response.ok(), path).toBe(true);
}
await page.goto("/ats-checker");
await expect(page.getByRole("navigation", { name: "Main navigation" })).toBeVisible();
await page.goto("/auth/login");
await expect(page.getByRole("button", { name: "Sign in", exact: true })).toBeVisible();
await owner.goto("/dashboard/resumes");
await expect(owner.getByText("Root Fixture", { exact: true }).first()).toBeVisible();
expect(failures).toEqual([]);
expect(fontStatuses.length).toBeGreaterThan(0);
expect(assetStatuses.length).toBeGreaterThan(0);
expect([...fontStatuses, ...assetStatuses].every((status) => status >= 200 && status < 400)).toBe(true);
expect((await owner.request.delete(resource)).ok()).toBe(true);
await unavailable();
} finally {
await visitor.close();
await other?.close();
await otherBootstrap.close();
await deleteE2EUser(secondAccount);
await pool.end();
}
});
+1
View File
@@ -48,6 +48,7 @@
"PORT",
"SERVER_PORT",
"APP_URL",
"ROOT_RESUME_ID",
"DATABASE_URL",
"AUTH_SECRET",
"BETTER_AUTH_API_KEY",