diff --git a/src/application/ports/auth_ports.rs b/src/application/ports/auth_ports.rs index 36845a10..4f5bf126 100644 --- a/src/application/ports/auth_ports.rs +++ b/src/application/ports/auth_ports.rs @@ -164,6 +164,13 @@ pub struct OidcIdClaims { pub email_verified: Option, pub preferred_username: Option, pub name: Option, + /// Standard OpenID claim `given_name` (first name). Populated on the + /// `User` row at JIT provisioning so the share-modal autocomplete and + /// the system address book can surface real names instead of just the + /// (often-cryptic) `preferred_username`. + pub given_name: Option, + /// Standard OpenID claim `family_name` (last name). See `given_name`. + pub family_name: Option, pub groups: Vec, pub picture: Option, } diff --git a/src/application/services/auth_application_service.rs b/src/application/services/auth_application_service.rs index ee80a51c..66d8a903 100644 --- a/src/application/services/auth_application_service.rs +++ b/src/application/services/auth_application_service.rs @@ -1641,6 +1641,8 @@ impl AuthApplicationService { email: user_info.email.or(claims.email), preferred_username: user_info.preferred_username.or(claims.preferred_username), name: user_info.name.or(claims.name), + given_name: user_info.given_name.or(claims.given_name), + family_name: user_info.family_name.or(claims.family_name), email_verified: user_info.email_verified.or(claims.email_verified), groups: if user_info.groups.is_empty() { claims.groups @@ -1797,6 +1799,8 @@ impl AuthApplicationService { ) })?; new_user.set_image(claims.picture.clone()); + new_user.set_given_name(claims.given_name.clone()); + new_user.set_family_name(claims.family_name.clone()); let created_user = self.user_storage.create_user(new_user).await?; diff --git a/src/infrastructure/services/oidc_service.rs b/src/infrastructure/services/oidc_service.rs index 19144e66..0d8b8f6e 100644 --- a/src/infrastructure/services/oidc_service.rs +++ b/src/infrastructure/services/oidc_service.rs @@ -65,6 +65,8 @@ struct IdTokenClaims { email_verified: Option, preferred_username: Option, name: Option, + given_name: Option, + family_name: Option, groups: Option>, nonce: Option, picture: Option, @@ -90,6 +92,8 @@ struct UserInfoResponse { email_verified: Option, preferred_username: Option, name: Option, + given_name: Option, + family_name: Option, groups: Option>, picture: Option, } @@ -461,6 +465,8 @@ impl OidcServicePort for OidcService { email_verified: claims.email_verified, preferred_username: claims.preferred_username, name: claims.name, + given_name: claims.given_name, + family_name: claims.family_name, groups: claims.groups.unwrap_or_default(), picture: claims.picture, }) @@ -513,6 +519,8 @@ impl OidcServicePort for OidcService { email_verified: info.email_verified, preferred_username: info.preferred_username, name: info.name, + given_name: info.given_name, + family_name: info.family_name, groups: info.groups.unwrap_or_default(), picture: info.picture, }) diff --git a/src/interfaces/api/handlers/contacts_handler.rs b/src/interfaces/api/handlers/contacts_handler.rs index 1ee98be2..97c5e5c5 100644 --- a/src/interfaces/api/handlers/contacts_handler.rs +++ b/src/interfaces/api/handlers/contacts_handler.rs @@ -182,14 +182,25 @@ fn if_match_passes(if_match: Option<&str>, stored_etag: &str) -> bool { /// Map a `UserDto` to a `ContactDto` so OxiCloud users appear as contacts /// inside the virtual system address book. +/// +/// `given_name`/`family_name` come from OIDC standard claims at JIT +/// provisioning (or NULL for password-only or pre-OIDC users). When +/// they're present, prefer a "First Last" full name; otherwise fall +/// back to the username (which is always present). fn user_to_contact(user: UserDto) -> ContactDto { + let full_name = match (user.given_name.as_deref(), user.family_name.as_deref()) { + (Some(g), Some(f)) => format!("{g} {f}"), + (Some(g), None) => g.to_string(), + (None, Some(f)) => f.to_string(), + (None, None) => user.username.clone(), + }; ContactDto { id: user.id.clone(), address_book_id: SYSTEM_BOOK_ID.to_string(), uid: format!("{}@oxicloud", user.id), - full_name: Some(user.username.clone()), - first_name: None, - last_name: None, + full_name: Some(full_name), + first_name: user.given_name.clone(), + last_name: user.family_name.clone(), nickname: None, email: vec![EmailDto { email: user.email,