feat(web-next): resolve the session user + live compliance context
The tRPC createContext was () => ({}) — the mounted dsr/consent routers
401'd every call and the dsr singleton stub threw (audit finding A11).
createTrpcContext now accepts an app resolveUser hook; web-next resolves
the session cookie through the auth feature's validateSession (denylist
included) plus a role snapshot, and threads bindProductionDsr/Consent
(or dev-seed) bindings into every request. The dsr router resolves its
binding from ctx.dsrBinding per request instead of a throwing proxy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
90
apps/web-next/src/server/trpc-context.test.ts
Normal file
90
apps/web-next/src/server/trpc-context.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const findByID = vi.fn();
|
||||
vi.mock("payload", () => ({
|
||||
getPayload: vi.fn(async () => ({ findByID })),
|
||||
}));
|
||||
vi.mock("@repo/core-cms", () => ({ default: Promise.resolve({}) }));
|
||||
|
||||
const validateSession = vi.fn();
|
||||
vi.mock("@repo/auth/di/container", () => ({
|
||||
authContainer: { get: () => ({ validateSession }) },
|
||||
}));
|
||||
|
||||
const consentFactory = vi.fn(async () => ({}));
|
||||
const dsrBinding = { marker: "dsr-binding" };
|
||||
const resolveBindingMode = vi.fn<() => "production" | "dev-seed">(
|
||||
() => "production",
|
||||
);
|
||||
vi.mock("./bind-production", () => ({
|
||||
bindAll: vi.fn(async () => {}),
|
||||
getComplianceBindings: vi.fn(async () => ({ consentFactory, dsrBinding })),
|
||||
resolveBindingMode: () => resolveBindingMode(),
|
||||
}));
|
||||
|
||||
import { createWebNextTrpcContext } from "./trpc-context";
|
||||
|
||||
function makeRequest(headers: Record<string, string> = {}): Request {
|
||||
return new Request("https://example.test/api/trpc", { headers });
|
||||
}
|
||||
|
||||
describe("createWebNextTrpcContext (A11)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resolveBindingMode.mockReturnValue("production");
|
||||
findByID.mockResolvedValue({ id: "user-1", role: "admin" });
|
||||
validateSession.mockResolvedValue({ user: { id: "user-1" } });
|
||||
});
|
||||
|
||||
it("threads compliance bindings for anonymous requests", async () => {
|
||||
const ctx = await createWebNextTrpcContext(makeRequest());
|
||||
expect(ctx.user).toBeUndefined();
|
||||
expect(ctx.userId).toBeUndefined();
|
||||
expect(ctx.consentFactory).toBe(consentFactory);
|
||||
expect(ctx.dsrBinding).toBe(dsrBinding);
|
||||
expect(validateSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("derives clientIp from proxy headers (B2)", async () => {
|
||||
const ctx = await createWebNextTrpcContext(
|
||||
makeRequest({ "x-forwarded-for": "203.0.113.9" }),
|
||||
);
|
||||
expect(ctx.clientIp).toBe("203.0.113.9");
|
||||
});
|
||||
|
||||
it("resolves the user + role snapshot from the payload-token cookie", async () => {
|
||||
const ctx = await createWebNextTrpcContext(
|
||||
makeRequest({ cookie: "payload-token=jwt-abc; other=1" }),
|
||||
);
|
||||
expect(validateSession).toHaveBeenCalledWith("jwt-abc");
|
||||
expect(ctx.user).toEqual({ id: "user-1", roles: ["admin"] });
|
||||
expect(ctx.userId).toBe("user-1");
|
||||
});
|
||||
|
||||
it("resolves the dev-seed session cookie name too", async () => {
|
||||
resolveBindingMode.mockReturnValue("dev-seed");
|
||||
const ctx = await createWebNextTrpcContext(
|
||||
makeRequest({ cookie: "session=session_user-1" }),
|
||||
);
|
||||
expect(validateSession).toHaveBeenCalledWith("session_user-1");
|
||||
// dev-seed has no Payload — role snapshot is empty
|
||||
expect(ctx.user).toEqual({ id: "user-1", roles: [] });
|
||||
expect(findByID).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats an invalid/expired session as anonymous", async () => {
|
||||
validateSession.mockRejectedValue(new Error("Invalid or expired"));
|
||||
const ctx = await createWebNextTrpcContext(
|
||||
makeRequest({ cookie: "payload-token=tampered" }),
|
||||
);
|
||||
expect(ctx.user).toBeUndefined();
|
||||
});
|
||||
|
||||
it("yields no roles when the users doc has none", async () => {
|
||||
findByID.mockResolvedValue({ id: "user-1" });
|
||||
const ctx = await createWebNextTrpcContext(
|
||||
makeRequest({ cookie: "payload-token=jwt-abc" }),
|
||||
);
|
||||
expect(ctx.user).toEqual({ id: "user-1", roles: [] });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user