From 3533c9a13194628f43949616410d63cd5033991d Mon Sep 17 00:00:00 2001 From: Mahakisore7 Date: Sun, 20 Sep 2026 19:10:00 +0530 Subject: [PATCH 1/2] fix: decode real Cognito ID tokens' array-shaped cognito:groups claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit decodeIdToken (apps/web/src/lib/cognito.ts) only handled cognito:groups as a string, which is the shape API Gateway's HTTP API JWT authorizer flattens it to server-side (authContext.ts). A real Cognito-issued ID token always encodes it as a genuine JSON array (e.g. ["owner"]) — the client-side decode never matched that shape, so every real signed-in user's role silently resolved to undefined, hiding the Dashboard/Staff nav links for owners and managers on the live site. Only caught because a from-scratch signed-in test session showed no Dashboard link despite the account being in the owner group. Also fixes the local dev server's CORS config to allow the Authorization header — apps/web always attaches a real bearer token now, even against the local API server (which ignores it, per authContext.ts's documented fallback), but the browser's preflight was rejecting the header before the request ever reached that fallback. Co-Authored-By: Claude Sonnet 5 --- apps/api/src/local/server.ts | 2 +- apps/web/src/lib/cognito.ts | 12 +++++++++++- apps/web/test/lib/cognito.test.ts | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) 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(); From 188bc119c2c7b15e3e0fca7f968b20e1f3fe587c Mon Sep 17 00:00:00 2001 From: Mahakisore7 Date: Sun, 20 Sep 2026 19:10:31 +0530 Subject: [PATCH 2/2] chore: gitignore demo-assets/ (local video recordings, not for source control) Co-Authored-By: Claude Sonnet 5 --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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/