mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-04 02:33:47 +10:00
feat(sharing): serve a configured public resume at root (#3470)
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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" });
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user