fix(frontend): align NextAuth authorize() with flat backend LoginResponse
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).
This commit is contained in:
@@ -2,6 +2,20 @@ import NextAuth from "next-auth"
|
|||||||
|
|
||||||
import Credentials from "next-auth/providers/credentials"
|
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<string, unknown> {
|
||||||
|
try {
|
||||||
|
const payload = token.split(".")[1]
|
||||||
|
const json = Buffer.from(payload, "base64url").toString("utf8")
|
||||||
|
return JSON.parse(json) as Record<string, unknown>
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Helper: fetch with an AbortController timeout (default 5s) */
|
/** Helper: fetch with an AbortController timeout (default 5s) */
|
||||||
async function fetchWithTimeout(
|
async function fetchWithTimeout(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -42,14 +56,17 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
|
|
||||||
if (!res.ok) return null
|
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 data = await res.json()
|
||||||
|
const claims = decodeJwtPayload(data.accessToken)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: data.member.id,
|
id: (claims.sub as string) ?? data.accessToken,
|
||||||
email: data.member.email,
|
email: (claims.email as string) ?? credentials.email,
|
||||||
name: data.member.clubName,
|
name: (claims.email as string) ?? credentials.email,
|
||||||
role: data.member.role,
|
role: data.role ?? (claims.role as string),
|
||||||
clubId: data.member.clubId,
|
clubId: (claims.tenant_id as string) ?? "",
|
||||||
accessToken: data.accessToken,
|
accessToken: data.accessToken,
|
||||||
refreshToken: data.refreshToken,
|
refreshToken: data.refreshToken,
|
||||||
expiresAt: Date.now() + data.expiresIn * 1000,
|
expiresAt: Date.now() + data.expiresIn * 1000,
|
||||||
|
|||||||
Reference in New Issue
Block a user