Phase6c: Optional Company contact name
All checks were successful
Build and Push / build (push) Successful in 1m42s

This commit is contained in:
2026-05-25 20:21:26 +02:00
parent 323786672f
commit 9cd9879a18

View File

@@ -534,20 +534,24 @@ export async function registerCustomer(params: {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**
* Update a human user's profile (first name + last name). Returns * Update a human user's profile (first name + last name + display
* the new `details.changeDate` from ZITADEL so the caller can * name). Returns the new `details.changeDate` from ZITADEL so the
* confirm the write landed. * caller can confirm the write landed.
* *
* The v2 user service endpoint is technically a PUT but accepts * The v2 user service endpoint is technically a PUT but accepts
* partial bodies — only `profile.givenName` and `profile.familyName` * partial bodies — only the `profile` block is sent. ZITADEL
* are sent. ZITADEL preserves email, password, and other fields * preserves email, password, and other fields across the call
* across the call (verified empirically in stripe-node#7786 and * (verified empirically in zitadel-server#7786 and documented in
* documented in v2.63+ of zitadel-server). * v2.63+ of zitadel-server).
* *
* `displayName` is intentionally NOT sent. ZITADEL recomputes it * `displayName` IS sent explicitly, set to "givenName familyName".
* from givenName + familyName when not provided, which is what we * Empirically (and contra what some docs suggest), ZITADEL does
* want — keeping displayName as a frozen value would let it drift * NOT recompute displayName when only the name parts change — it
* out of sync with the name parts on subsequent edits. * keeps whatever displayName was previously stored, including the
* one set at user creation time. That stale displayName is what
* ZITADEL surfaces in the OIDC `name` claim, so without this
* explicit write the portal session would never see the updated
* name (even after sign-out / sign-in).
* *
* Auth: the portal's service-account PAT (ZITADEL_SA_PAT). The PAT * Auth: the portal's service-account PAT (ZITADEL_SA_PAT). The PAT
* must have user-write permission in the user's resource org. * must have user-write permission in the user's resource org.
@@ -556,7 +560,8 @@ export async function registerCustomer(params: {
*/ */
export interface UpdateHumanUserProfileResult { export interface UpdateHumanUserProfileResult {
changeDate: string; changeDate: string;
/** ZITADEL recomputes this from given+family unless overridden. */ /** The displayName ZITADEL stored, which the OIDC `name` claim will
* carry on the user's next session. */
displayName: string; displayName: string;
} }
@@ -566,6 +571,11 @@ export async function updateHumanUserProfile(params: {
familyName: string; familyName: string;
}): Promise<UpdateHumanUserProfileResult> { }): Promise<UpdateHumanUserProfileResult> {
const path = `/v2/users/human/${encodeURIComponent(params.userId)}`; const path = `/v2/users/human/${encodeURIComponent(params.userId)}`;
// Compose the displayName ourselves so ZITADEL stores something
// sensible. Empty-string fallback only triggers if both name parts
// are blank, which the API zod schema prevents anyway.
const displayName =
`${params.givenName.trim()} ${params.familyName.trim()}`.trim();
type ZitadelUpdateResponse = { type ZitadelUpdateResponse = {
details?: { changeDate?: string }; details?: { changeDate?: string };
}; };
@@ -573,15 +583,16 @@ export async function updateHumanUserProfile(params: {
profile: { profile: {
givenName: params.givenName, givenName: params.givenName,
familyName: params.familyName, familyName: params.familyName,
displayName,
}, },
}); });
// Re-fetch the user so we can return the canonical displayName // Re-fetch the user to read back the canonical displayName ZITADEL
// (ZITADEL computes "Given Family" itself; matching what NextAuth // committed. Should match what we sent, but reading from the source
// sees in the next sign-in claim). // of truth catches any sanitization ZITADEL might apply.
const detail = await getHumanUserDetail(params.userId); const detail = await getHumanUserDetail(params.userId);
return { return {
changeDate: new Date().toISOString(), changeDate: new Date().toISOString(),
displayName: detail.displayName, displayName: detail.displayName || displayName,
}; };
} }