diff --git a/.gitignore b/.gitignore index 2385ede..2ae8ed9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ test-results/ playwright-report/ blob-report/ playwright/.cache/ +demo-assets/ diff --git a/apps/api/src/local/server.ts b/apps/api/src/local/server.ts index 9669f84..a541826 100644 --- a/apps/api/src/local/server.ts +++ b/apps/api/src/local/server.ts @@ -216,7 +216,7 @@ async function main(): Promise { app.use(express.json()); app.use((_req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Headers", "content-type, x-api-key"); + res.header("Access-Control-Allow-Headers", "content-type, x-api-key, authorization"); next(); }); diff --git a/apps/web/src/lib/cognito.ts b/apps/web/src/lib/cognito.ts index afaf5e4..0edac0a 100644 --- a/apps/web/src/lib/cognito.ts +++ b/apps/web/src/lib/cognito.ts @@ -40,7 +40,17 @@ export interface IdTokenClaims { export function decodeIdToken(idToken: string): IdTokenClaims { const [, payloadB64] = idToken.split("."); const payload = JSON.parse(atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/"))) as Record; - const groups = typeof payload["cognito:groups"] === "string" ? (payload["cognito:groups"] as string) : ""; + // A real Cognito ID token encodes `cognito:groups` as a genuine JSON + // array (e.g. ["owner"]); API Gateway's HTTP API JWT authorizer is the + // only place that ever flattens it to a comma-separated string (see + // authContext.ts) — this decodes the token directly, so both shapes + // need handling here. + const rawGroups = payload["cognito:groups"]; + const groups = Array.isArray(rawGroups) + ? rawGroups.map(String) + : typeof rawGroups === "string" + ? rawGroups.split(",").map((group) => group.trim()) + : []; const role = (["owner", "manager", "counter_staff"] as const).find((candidate) => groups.includes(candidate)); return { sub: payload.sub as string, diff --git a/apps/web/test/lib/cognito.test.ts b/apps/web/test/lib/cognito.test.ts index 59b490c..0c464ce 100644 --- a/apps/web/test/lib/cognito.test.ts +++ b/apps/web/test/lib/cognito.test.ts @@ -37,11 +37,16 @@ describe("decodeIdToken", () => { expect(decodeIdToken(token)).toEqual({ sub: "abc", email: "a@b.com", shopId: "shop-123", role: undefined }); }); - it("resolves the role from a comma-flattened cognito:groups claim", () => { + it("resolves the role from a comma-flattened cognito:groups claim (API Gateway JWT authorizer shape)", () => { const token = fakeIdToken({ sub: "abc", "cognito:groups": "owner" }); expect(decodeIdToken(token).role).toBe("owner"); }); + it("resolves the role from a real Cognito ID token's array-shaped cognito:groups claim", () => { + const token = fakeIdToken({ sub: "abc", "cognito:groups": ["owner"] }); + expect(decodeIdToken(token).role).toBe("owner"); + }); + it("returns undefined role when no known group is present", () => { const token = fakeIdToken({ sub: "abc" }); expect(decodeIdToken(token).role).toBeUndefined();