Phase6c: Optional Company contact name
All checks were successful
Build and Push / build (push) Successful in 1m40s
All checks were successful
Build and Push / build (push) Successful in 1m40s
This commit is contained in:
@@ -49,7 +49,26 @@ export const authConfig: NextAuthConfig = {
|
||||
},
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, account, profile }) {
|
||||
async jwt({ token, account, profile, trigger, session }) {
|
||||
// Phase 6 fix5: client-side `useSession().update({ name })` calls
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
if (trigger === "update" && session) {
|
||||
const update = session as { name?: unknown };
|
||||
if (typeof update.name === "string") {
|
||||
(token as { name?: string }).name = update.name;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
if (account && profile) {
|
||||
const claims = profile as unknown as ZitadelClaims;
|
||||
token.orgId = claims["urn:zitadel:iam:user:resourceowner:id"];
|
||||
|
||||
@@ -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 ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user