From 323786672f5d5833818ec25b502f19c01e56a5ac Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 25 May 2026 14:08:18 +0200 Subject: [PATCH] Phase6c: Optional Company contact name --- src/components/settings/profile-form.tsx | 24 ++++++++---- src/lib/auth.ts | 47 +++++++++++++++++++++--- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/components/settings/profile-form.tsx b/src/components/settings/profile-form.tsx index cdd69e2..9aca729 100644 --- a/src/components/settings/profile-form.tsx +++ b/src/components/settings/profile-form.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import { useRouter } from "next/navigation"; import { useSession } from "next-auth/react"; import { useTranslations } from "next-intl"; import { Card } from "@/components/ui/card"; @@ -35,15 +34,15 @@ interface Props { * On save, we trigger NextAuth's `update()` from useSession() with * the new display name. That routes through our jwt callback * (trigger='update' branch) which overlays token.name without a - * logout/login. The whole UI sees the new name on the next render. - * - * router.refresh() additionally re-runs the server component, so - * the page's own server-fetched values pick up the new state if the - * user immediately returns. + * logout/login. After the cookie is updated we trigger a full page + * reload — every server-rendered surface (nav-shell, dashboard + * welcome, instance cards) re-reads the cookie on the next request + * and renders with the new name. router.refresh() alone wasn't + * enough: it re-runs only the current route's server components, + * leaving outer-tree segments stale until the user navigates. */ export function ProfileSettingsForm({ initial, isPersonal, orgName }: Props) { const t = useTranslations("settingsProfile"); - const router = useRouter(); const { update } = useSession(); const [form, setForm] = useState({ firstName: initial.firstName, @@ -80,7 +79,16 @@ export function ProfileSettingsForm({ initial, isPersonal, orgName }: Props) { // to session.user.name. No re-login needed. await update({ name: data.displayName }); setSavedFlash(true); - router.refresh(); + // Force a full reload so EVERY server-rendered component picks + // up the new session cookie immediately — router.refresh() only + // re-runs the current route's server components, leaving the + // nav-shell (rendered higher in the tree) and other cached + // segments showing the old name until the user navigates. + // The 800ms delay lets the "Saved" flash render briefly before + // the page reloads, so the user gets visible feedback. + setTimeout(() => { + window.location.reload(); + }, 800); } catch (e: any) { setError(e?.message ?? String(e)); } finally { diff --git a/src/lib/auth.ts b/src/lib/auth.ts index deba380..6798985 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -54,18 +54,23 @@ export const authConfig: NextAuthConfig = { // route through this branch. We trust the new value because the // PUT /api/settings/profile route already wrote it to ZITADEL // and re-fetched the canonical displayName before returning. - // NextAuth maps token.name → session.user.name on the next - // session callback, so downstream useSession() consumers see - // the new name without a logout/login cycle. + // The session callback reads token.name directly (see below) so + // the update propagates without depending on auth.js's implicit + // token→session.user mapping, which is flaky for the name claim + // in the v5 OIDC provider configuration. // // Defensive: only the `name` field is accepted from the update // payload, even if the client passes additional keys. Other // identity claims (orgId, roles, sub) come from ZITADEL at // sign-in time and are not user-mutable from a settings page. + // + // Returns a NEW token object (spread) rather than mutating, so + // there is no ambiguity for auth.js about whether the token + // changed and needs re-encoding into the session cookie. if (trigger === "update" && session) { const update = session as { name?: unknown }; if (typeof update.name === "string") { - (token as { name?: string }).name = update.name; + return { ...token, name: update.name }; } return token; } @@ -77,6 +82,19 @@ export const authConfig: NextAuthConfig = { claims["urn:zitadel:iam:org:project:roles"] ); token.accessToken = account.access_token; + // Phase 6 fix5: explicitly pin the standard name/email claims + // onto the token from the OIDC profile. Previously these came + // through auth.js's implicit mapping, which works on first + // sign-in but isn't reliable after update() — once the update + // path overrides token.name, the read-back path needs token + // to be the authoritative source. Setting them explicitly + // here keeps sign-in and update on the same path. + if (typeof profile.name === "string") { + token.name = profile.name; + } + if (typeof profile.email === "string") { + token.email = profile.email; + } // Pin token.sub to the OIDC subject. Auth.js v5 otherwise puts a // freshly generated UUID in token.sub on initial sign-in, // ignoring what profile() returns for `id`. That UUID then @@ -99,10 +117,19 @@ export const authConfig: NextAuthConfig = { async session({ session, token }) { const roles = (token.roles as Role[]) ?? []; const orgName = (token.orgName as string) ?? ""; + // Phase 6 fix5: read name and email directly from the token. + // Previously this code relied on `session.user?.name`, expecting + // auth.js to map token.name → session.user.name automatically. + // That mapping is brittle: it works on first sign-in (because + // OIDC profile() populates session.user) but not after update() + // overrides token.name. Reading from token is the canonical + // path regardless of how the token was last written. + const tokenName = (token.name as string | undefined) ?? ""; + const tokenEmail = (token.email as string | undefined) ?? ""; const sessionUser: SessionUser = { id: token.sub!, - name: session.user?.name ?? "", - email: session.user?.email ?? "", + name: tokenName || session.user?.name || "", + email: tokenEmail || session.user?.email || "", orgId: token.orgId as string, orgName, roles, @@ -115,6 +142,14 @@ export const authConfig: NextAuthConfig = { isPersonal: isPersonalOrgName(orgName), }; (session as any).platformUser = sessionUser; + // Also overwrite session.user so any client-side code that uses + // the standard NextAuth shape (session.user.name) sees the new + // value. Pre-fix5 code paths read from session.user.name; this + // keeps them working without per-component changes. + if (session.user) { + session.user.name = sessionUser.name; + session.user.email = sessionUser.email; + } return session; }, },