diff --git a/app/queries/create_user.graphql b/app/queries/create_user.graphql index bf269e0..e669091 100644 --- a/app/queries/create_user.graphql +++ b/app/queries/create_user.graphql @@ -1,4 +1,4 @@ -mutation CreateUser($user: UserInput!) { +mutation CreateUser($user: CreateUserInput!) { createUser(user: $user) { id creationDate diff --git a/app/src/create_user.rs b/app/src/create_user.rs index 5140c48..591b503 100644 --- a/app/src/create_user.rs +++ b/app/src/create_user.rs @@ -46,7 +46,7 @@ impl CreateUserForm { match msg { Msg::SubmitForm => { let req = create_user::Variables { - user: create_user::UserInput { + user: create_user::CreateUserInput { id: get_element("username") .filter(not_empty) .ok_or_else(|| anyhow!("Missing username"))?, diff --git a/schema.graphql b/schema.graphql index 6a22b94..2a0c2b2 100644 --- a/schema.graphql +++ b/schema.graphql @@ -4,7 +4,8 @@ input EqualityConstraint { } type Mutation { - createUser(user: UserInput!): User! + createUser(user: CreateUserInput!): User! + updateUser(user: UpdateUserInput!): Success! } type Group { @@ -28,7 +29,7 @@ input RequestFilter { scalar DateTimeUtc "The details required to create a user." -input UserInput { +input CreateUserInput { id: String! email: String! displayName: String @@ -53,6 +54,19 @@ type User { groups: [Group!]! } +type Success { + ok: Boolean! +} + +"The fields that can be updated for a user." +input UpdateUserInput { + id: String! + email: String + displayName: String + firstName: String + lastName: String +} + schema { query: Query mutation: Mutation diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index 72c3f21..904de24 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -59,6 +59,16 @@ pub struct CreateUserRequest { pub last_name: Option, } +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)] +pub struct UpdateUserRequest { + // Same fields as CreateUserRequest, but no with an extra layer of Option. + pub user_id: String, + pub email: Option, + pub display_name: Option, + pub first_name: Option, + pub last_name: Option, +} + #[async_trait] pub trait LoginHandler: Clone + Send { async fn bind(&self, request: BindRequest) -> Result<()>; @@ -73,6 +83,7 @@ pub trait BackendHandler: Clone + Send { async fn list_groups(&self) -> Result>; async fn get_user_details(&self, user_id: &str) -> Result; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; + async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn delete_user(&self, user_id: &str) -> Result<()>; async fn create_group(&self, group_name: &str) -> Result; async fn add_user_to_group(&self, user_id: &str, group_id: GroupId) -> Result<()>; @@ -91,6 +102,7 @@ mockall::mock! { async fn list_groups(&self) -> Result>; async fn get_user_details(&self, user_id: &str) -> Result; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; + async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; async fn delete_user(&self, user_id: &str) -> Result<()>; async fn create_group(&self, group_name: &str) -> Result; async fn get_user_groups(&self, user: &str) -> Result>; diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs index 8c6fbb6..797a366 100644 --- a/server/src/domain/sql_backend_handler.rs +++ b/server/src/domain/sql_backend_handler.rs @@ -193,6 +193,31 @@ impl BackendHandler for SqlBackendHandler { Ok(()) } + async fn update_user(&self, request: UpdateUserRequest) -> Result<()> { + let mut values = Vec::new(); + if let Some(email) = request.email { + values.push((Users::Email, email.into())); + } + if let Some(display_name) = request.display_name { + values.push((Users::DisplayName, display_name.into())); + } + if let Some(first_name) = request.first_name { + values.push((Users::FirstName, first_name.into())); + } + if let Some(last_name) = request.last_name { + values.push((Users::LastName, last_name.into())); + } + if values.is_empty() { + return Ok(()); + } + let query = Query::update() + .table(Users::Table) + .values(values) + .to_string(DbQueryBuilder {}); + sqlx::query(&query).execute(&self.sql_pool).await?; + Ok(()) + } + async fn delete_user(&self, user_id: &str) -> Result<()> { let delete_query = Query::delete() .from_table(Users::Table) diff --git a/server/src/infra/graphql/mutation.rs b/server/src/infra/graphql/mutation.rs index a4f162a..67801d7 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/server/src/infra/graphql/mutation.rs @@ -1,5 +1,5 @@ -use crate::domain::handler::{BackendHandler, CreateUserRequest}; -use juniper::{graphql_object, FieldResult, GraphQLInputObject}; +use crate::domain::handler::{BackendHandler, CreateUserRequest, UpdateUserRequest}; +use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject}; use super::api::Context; @@ -19,7 +19,7 @@ impl Mutation { #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] /// The details required to create a user. -pub struct UserInput { +pub struct CreateUserInput { id: String, email: String, display_name: Option, @@ -27,11 +27,32 @@ pub struct UserInput { last_name: Option, } +#[derive(PartialEq, Eq, Debug, GraphQLInputObject)] +/// The fields that can be updated for a user. +pub struct UpdateUserInput { + id: String, + email: Option, + display_name: Option, + first_name: Option, + last_name: Option, +} + +#[derive(PartialEq, Eq, Debug, GraphQLObject)] +pub struct Success { + ok: bool, +} + +impl Success { + fn new() -> Self { + Self { ok: true } + } +} + #[graphql_object(context = Context)] impl Mutation { async fn create_user( context: &Context, - user: UserInput, + user: CreateUserInput, ) -> FieldResult> { if !context.validation_result.is_admin { return Err("Unauthorized user creation".into()); @@ -52,4 +73,24 @@ impl Mutation { .await .map(Into::into)?) } + + async fn update_user( + context: &Context, + user: UpdateUserInput, + ) -> FieldResult { + if !context.validation_result.can_access(&user.id) { + return Err("Unauthorized user update".into()); + } + context + .handler + .update_user(UpdateUserRequest { + user_id: user.id, + email: user.email, + display_name: user.display_name, + first_name: user.first_name, + last_name: user.last_name, + }) + .await?; + Ok(Success::new()) + } } diff --git a/server/src/infra/tcp_backend_handler.rs b/server/src/infra/tcp_backend_handler.rs index ed896cc..08935ee 100644 --- a/server/src/infra/tcp_backend_handler.rs +++ b/server/src/infra/tcp_backend_handler.rs @@ -31,6 +31,7 @@ mockall::mock! { async fn get_user_details(&self, user_id: &str) -> DomainResult; async fn get_user_groups(&self, user: &str) -> DomainResult>; async fn create_user(&self, request: CreateUserRequest) -> DomainResult<()>; + async fn update_user(&self, request: UpdateUserRequest) -> DomainResult<()>; async fn delete_user(&self, user_id: &str) -> DomainResult<()>; async fn create_group(&self, group_name: &str) -> DomainResult; async fn add_user_to_group(&self, user_id: &str, group_id: GroupId) -> DomainResult<()>;