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

This commit is contained in:
2026-05-25 13:50:16 +02:00
parent 002867850d
commit a1769eeb00
10 changed files with 530 additions and 7 deletions

View File

@@ -528,3 +528,102 @@ export async function registerCustomer(params: {
throw err;
}
}
// ---------------------------------------------------------------------------
// v2 User API — profile updates (Phase 6 fix5)
// ---------------------------------------------------------------------------
/**
* Update a human user's profile (first name + last name). Returns
* the new `details.changeDate` from ZITADEL so the caller can
* confirm the write landed.
*
* The v2 user service endpoint is technically a PUT but accepts
* partial bodies — only `profile.givenName` and `profile.familyName`
* are sent. ZITADEL preserves email, password, and other fields
* across the call (verified empirically in stripe-node#7786 and
* documented in v2.63+ of zitadel-server).
*
* `displayName` is intentionally NOT sent. ZITADEL recomputes it
* from givenName + familyName when not provided, which is what we
* want — keeping displayName as a frozen value would let it drift
* out of sync with the name parts on subsequent edits.
*
* Auth: the portal's service-account PAT (ZITADEL_SA_PAT). The PAT
* must have user-write permission in the user's resource org.
* Today portal-zitadel-sa-pat already has user-write for
* createHumanUser etc. — same scope covers this.
*/
export interface UpdateHumanUserProfileResult {
changeDate: string;
/** ZITADEL recomputes this from given+family unless overridden. */
displayName: string;
}
export async function updateHumanUserProfile(params: {
userId: string;
givenName: string;
familyName: string;
}): Promise<UpdateHumanUserProfileResult> {
const path = `/v2/users/human/${encodeURIComponent(params.userId)}`;
type ZitadelUpdateResponse = {
details?: { changeDate?: string };
};
await zitadelFetch<ZitadelUpdateResponse>(path, "PUT", {
profile: {
givenName: params.givenName,
familyName: params.familyName,
},
});
// Re-fetch the user so we can return the canonical displayName
// (ZITADEL computes "Given Family" itself; matching what NextAuth
// sees in the next sign-in claim).
const detail = await getHumanUserDetail(params.userId);
return {
changeDate: new Date().toISOString(),
displayName: detail.displayName,
};
}
/**
* Fetch a human user's current profile (given/family/display name +
* email). Used by the settings page to populate the form and by the
* update helper above to read back the computed displayName.
*/
export interface HumanUserDetail {
userId: string;
givenName: string;
familyName: string;
displayName: string;
email: string;
}
export async function getHumanUserDetail(
userId: string
): Promise<HumanUserDetail> {
type ZitadelGetUserResponse = {
user?: {
userId?: string;
human?: {
profile?: {
givenName?: string;
familyName?: string;
displayName?: string;
};
email?: { email?: string };
};
};
};
const response = await zitadelFetch<ZitadelGetUserResponse>(
`/v2/users/${encodeURIComponent(userId)}`,
"GET"
);
const human = response.user?.human;
return {
userId: response.user?.userId ?? userId,
givenName: human?.profile?.givenName ?? "",
familyName: human?.profile?.familyName ?? "",
displayName: human?.profile?.displayName ?? "",
email: human?.email?.email ?? "",
};
}