feat: add getBrowserSessionUser for routes that need the caller's identity

This commit is contained in:
Dave 2026-09-01 01:33:10 +02:00
parent 5c210546e9
commit 652b60706e
2 changed files with 114 additions and 0 deletions

View file

@ -0,0 +1,62 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { NextRequest } from "next/server";
import { getBrowserSessionUser } from "./browser-session";
const ORIGINAL_FETCH = global.fetch;
function makeRequest(authHeader: string | null): NextRequest {
const headers = new Headers();
if (authHeader !== null) headers.set("authorization", authHeader);
return new NextRequest("http://localhost:4001/api/v1/rep-dashboard", {
headers,
});
}
describe("getBrowserSessionUser", () => {
beforeEach(() => {
global.fetch = ORIGINAL_FETCH;
});
afterEach(() => {
global.fetch = ORIGINAL_FETCH;
});
it("throws a 401-tagged error when no token is present", async () => {
await expect(getBrowserSessionUser(makeRequest(null))).rejects.toMatchObject({
status: 401,
});
});
it("throws a 401-tagged error when PB rejects the token", async () => {
global.fetch = vi.fn(async () => new Response("", { status: 401 })) as unknown as typeof fetch;
await expect(
getBrowserSessionUser(makeRequest("Bearer bad-token")),
).rejects.toMatchObject({ status: 401 });
});
it("returns the resolved user's id and user_type on success", async () => {
global.fetch = vi.fn(async () =>
new Response(
JSON.stringify({
token: "refreshed-token",
record: { id: "user123", user_type: ["REP"], name: "Test Rep" },
}),
{ status: 200, headers: { "content-type": "application/json" } },
),
) as unknown as typeof fetch;
const result = await getBrowserSessionUser(makeRequest("Bearer good-token"));
expect(result).toEqual({ id: "user123", user_type: ["REP"] });
});
it("defaults user_type to null when the record omits it", async () => {
global.fetch = vi.fn(async () =>
new Response(
JSON.stringify({ token: "t", record: { id: "user456" } }),
{ status: 200, headers: { "content-type": "application/json" } },
),
) as unknown as typeof fetch;
const result = await getBrowserSessionUser(makeRequest("Bearer good-token"));
expect(result).toEqual({ id: "user456", user_type: null });
});
});

View file

@ -35,3 +35,55 @@ export async function assertBrowserSession(req: NextRequest): Promise<void> {
});
}
}
export interface BrowserSessionUser {
id: string;
user_type: string[] | null;
}
/**
* Same validation as `assertBrowserSession`, but returns the resolved
* PB user record instead of discarding it — for routes that need to
* scope queries to "whoever is logged in" (e.g. the rep dashboard).
*/
export async function getBrowserSessionUser(
req: NextRequest,
): Promise<BrowserSessionUser> {
const authorization = req.headers.get("authorization") || "";
const token = authorization.startsWith("Bearer ")
? authorization.slice(7)
: authorization;
if (!token) {
throw Object.assign(new Error("Not authenticated."), { status: 401 });
}
const pbUrl =
process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090";
const response = await fetch(
`${pbUrl.replace(/\/$/, "")}/api/collections/users/auth-refresh`,
{
method: "POST",
headers: { Authorization: token },
signal: AbortSignal.timeout(5_000),
},
);
if (!response.ok) {
throw Object.assign(new Error("Invalid or expired session."), {
status: 401,
});
}
const body = (await response.json()) as {
record?: { id?: string; user_type?: string[] | null };
};
const id = body.record?.id;
if (!id) {
throw Object.assign(new Error("Invalid or expired session."), {
status: 401,
});
}
return { id, user_type: body.record?.user_type ?? null };
}