From 281adda27c4f2a284f4ab0f4c45f87c3026a48bb Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Sat, 13 Jun 2026 10:10:48 +0200 Subject: [PATCH] fix(frontend): align NextAuth authorize() with flat backend LoginResponse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Login reached the backend (HTTP 200) but NextAuth returned CredentialsSignin. Cause: authorize() read data.member.id/email/clubName/clubId, but the backend LoginResponse is flat — { accessToken, refreshToken, expiresIn, role } with no member object. Accessing data.member.id on undefined threw, so authorize() returned null. Decode the JWT payload to recover identity claims (sub=userId, email, tenant_id=clubId) and use the flat top-level role. Adds a small decodeJwtPayload helper (claims only, no signature verification needed here). --- cannamanage-frontend/src/lib/auth.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/cannamanage-frontend/src/lib/auth.ts b/cannamanage-frontend/src/lib/auth.ts index ad34a71..3bc7d8f 100644 --- a/cannamanage-frontend/src/lib/auth.ts +++ b/cannamanage-frontend/src/lib/auth.ts @@ -2,6 +2,20 @@ import NextAuth from "next-auth" import Credentials from "next-auth/providers/credentials" +/** + * Decode a JWT payload (no signature verification — we only need the claims for + * populating the session). Returns {} on any parse failure. + */ +function decodeJwtPayload(token: string): Record { + try { + const payload = token.split(".")[1] + const json = Buffer.from(payload, "base64url").toString("utf8") + return JSON.parse(json) as Record + } catch { + return {} + } +} + /** Helper: fetch with an AbortController timeout (default 5s) */ async function fetchWithTimeout( url: string, @@ -42,14 +56,17 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ if (!res.ok) return null + // Backend LoginResponse is flat: { accessToken, refreshToken, expiresIn, role }. + // Identity claims (sub=userId, email, tenant_id=clubId) live inside the JWT. const data = await res.json() + const claims = decodeJwtPayload(data.accessToken) return { - id: data.member.id, - email: data.member.email, - name: data.member.clubName, - role: data.member.role, - clubId: data.member.clubId, + id: (claims.sub as string) ?? data.accessToken, + email: (claims.email as string) ?? credentials.email, + name: (claims.email as string) ?? credentials.email, + role: data.role ?? (claims.role as string), + clubId: (claims.tenant_id as string) ?? "", accessToken: data.accessToken, refreshToken: data.refreshToken, expiresAt: Date.now() + data.expiresIn * 1000,