diff --git a/app/src/components/create_user.rs b/app/src/components/create_user.rs index a5cca69..c50777e 100644 --- a/app/src/components/create_user.rs +++ b/app/src/components/create_user.rs @@ -88,7 +88,7 @@ impl CommonComponent for CreateUserForm { let req = create_user::Variables { user: create_user::CreateUserInput { id: model.username, - email: model.email, + email: Some(model.email), displayName: to_option(model.display_name), firstName: to_option(model.first_name), lastName: to_option(model.last_name), diff --git a/migration-tool/src/ldap.rs b/migration-tool/src/ldap.rs index 60ff8dc..9fdad3a 100644 --- a/migration-tool/src/ldap.rs +++ b/migration-tool/src/ldap.rs @@ -189,7 +189,7 @@ impl TryFrom for User { Ok(User::new( crate::lldap::CreateUserInput { id, - email, + email: Some(email), display_name, first_name, last_name, diff --git a/schema.graphql b/schema.graphql index 6627ebd..f744e56 100644 --- a/schema.graphql +++ b/schema.graphql @@ -63,7 +63,7 @@ type Query { "The details required to create a user." input CreateUserInput { id: String! - email: String! + email: String displayName: String firstName: String lastName: String diff --git a/server/src/infra/graphql/mutation.rs b/server/src/infra/graphql/mutation.rs index f8d414a..cb68d70 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/server/src/infra/graphql/mutation.rs @@ -7,8 +7,9 @@ use crate::{ AttributeList, BackendHandler, CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest, }, + schema::PublicSchema, types::{ - AttributeName, AttributeType, AttributeValue as DomainAttributeValue, GroupId, + AttributeName, AttributeType, AttributeValue as DomainAttributeValue, Email, GroupId, JpegPhoto, LdapObjectClass, UserId, }, }, @@ -39,7 +40,7 @@ impl Mutation { } } -#[derive(PartialEq, Eq, Debug, GraphQLInputObject)] +#[derive(Clone, PartialEq, Eq, Debug, GraphQLInputObject)] // This conflicts with the attribute values returned by the user/group queries. #[graphql(name = "AttributeValueInput")] struct AttributeValue { @@ -58,7 +59,8 @@ struct AttributeValue { /// The details required to create a user. pub struct CreateUserInput { id: String, - email: String, + // The email can be specified as an attribute, but one of the two is required. + email: Option, display_name: Option, first_name: Option, last_name: Option, @@ -120,6 +122,44 @@ impl Success { } } +struct UnpackedAttributes { + email: Option, + display_name: Option, + attributes: Vec, +} + +fn unpack_attributes( + attributes: Vec, + schema: &PublicSchema, + is_admin: bool, +) -> FieldResult { + let email = attributes + .iter() + .find(|attr| attr.name == "mail") + .cloned() + .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin)) + .transpose()? + .map(|attr| attr.value.unwrap::()) + .map(Email::from); + let display_name = attributes + .iter() + .find(|attr| attr.name == "displayName") + .cloned() + .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin)) + .transpose()? + .map(|attr| attr.value.unwrap::()); + let attributes = attributes + .into_iter() + .filter(|attr| attr.name != "mail" && attr.name != "displayName") + .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin)) + .collect::, _>>()?; + Ok(UnpackedAttributes { + email, + display_name, + attributes, + }) +} + #[graphql_object(context = Context)] impl Mutation { async fn create_user( @@ -143,17 +183,20 @@ impl Mutation { .transpose() .context("Provided image is not a valid JPEG")?; let schema = handler.get_schema().await?; - let attributes = user - .attributes - .unwrap_or_default() - .into_iter() - .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, true)) - .collect::, _>>()?; + let UnpackedAttributes { + email, + display_name, + attributes, + } = unpack_attributes(user.attributes.unwrap_or_default(), &schema, true)?; handler .create_user(CreateUserRequest { user_id: user_id.clone(), - email: user.email.into(), - display_name: user.display_name, + email: user + .email + .map(Email::from) + .or(email) + .ok_or_else(|| anyhow!("Email is required when creating a new user"))?, + display_name: user.display_name.or(display_name), first_name: user.first_name, last_name: user.last_name, avatar, @@ -216,17 +259,17 @@ impl Mutation { .transpose() .context("Provided image is not a valid JPEG")?; let schema = handler.get_schema().await?; - let insert_attributes = user - .insert_attributes - .unwrap_or_default() - .into_iter() - .map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin)) - .collect::, _>>()?; + let user_insert_attributes = user.insert_attributes.unwrap_or_default(); + let UnpackedAttributes { + email, + display_name, + attributes: insert_attributes, + } = unpack_attributes(user_insert_attributes, &schema, is_admin)?; handler .update_user(UpdateUserRequest { user_id, - email: user.email.map(Into::into), - display_name: user.display_name, + email: user.email.map(Into::into).or(email), + display_name: user.display_name.or(display_name), first_name: user.first_name, last_name: user.last_name, avatar, diff --git a/server/tests/common/fixture.rs b/server/tests/common/fixture.rs index b970c2c..4e35577 100644 --- a/server/tests/common/fixture.rs +++ b/server/tests/common/fixture.rs @@ -103,7 +103,7 @@ impl LLDAPFixture { create_user::Variables { user: create_user::CreateUserInput { id: user.clone(), - email: format!("{}@lldap.test", user), + email: Some(format!("{}@lldap.test", user)), avatar: None, display_name: None, first_name: None,