use crate::domain::{ handler::{BackendHandler, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest}, types::{DisplayName, GroupId, JpegPhoto, UserId}, }; use anyhow::Context as AnyhowContext; use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject}; use tracing::{debug, debug_span, Instrument}; use super::api::Context; #[derive(PartialEq, Eq, Debug)] /// The top-level GraphQL mutation type. pub struct Mutation { _phantom: std::marker::PhantomData>, } impl Mutation { pub fn new() -> Self { Self { _phantom: std::marker::PhantomData, } } } #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] /// The details required to create a user. pub struct CreateUserInput { id: String, email: String, display_name: String, first_name: Option, last_name: Option, // Base64 encoded JpegPhoto. avatar: Option, } #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] /// The fields that can be updated for a user. pub struct UpdateUserInput { id: String, email: Option, display_name: String, first_name: Option, last_name: Option, // Base64 encoded JpegPhoto. avatar: Option, } #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] /// The fields that can be updated for a group. pub struct UpdateGroupInput { id: i32, display_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: CreateUserInput, ) -> FieldResult> { let span = debug_span!("[GraphQL mutation] create_user"); span.in_scope(|| { debug!(?user.id); }); if !context.validation_result.is_admin() { span.in_scope(|| debug!("Unauthorized")); return Err("Unauthorized user creation".into()); } let user_id = UserId::new(&user.id); let display_name = DisplayName::new(&user.display_name); let avatar = user .avatar .map(base64::decode) .transpose() .context("Invalid base64 image")? .map(JpegPhoto::try_from) .transpose() .context("Provided image is not a valid JPEG")?; context .handler .create_user(CreateUserRequest { user_id: user_id.clone(), email: user.email, display_name: display_name.clone(), first_name: user.first_name, last_name: user.last_name, avatar, }) .instrument(span.clone()) .await?; Ok(context .handler .get_user_details(&user_id) .instrument(span) .await .map(Into::into)?) } async fn create_group( context: &Context, name: String, ) -> FieldResult> { let span = debug_span!("[GraphQL mutation] create_group"); span.in_scope(|| { debug!(?name); }); if !context.validation_result.is_admin() { span.in_scope(|| debug!("Unauthorized")); return Err("Unauthorized group creation".into()); } let group_id = context.handler.create_group(&name).await?; Ok(context .handler .get_group_details(group_id) .instrument(span) .await .map(Into::into)?) } async fn update_user( context: &Context, user: UpdateUserInput, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] update_user"); span.in_scope(|| { debug!(?user.id); }); let user_id = UserId::new(&user.id); let display_name = DisplayName::new(&user.display_name); if !context.validation_result.can_write(&user_id) { span.in_scope(|| debug!("Unauthorized")); return Err("Unauthorized user update".into()); } let avatar = user .avatar .map(base64::decode) .transpose() .context("Invalid base64 image")? .map(JpegPhoto::try_from) .transpose() .context("Provided image is not a valid JPEG")?; context .handler .update_user(UpdateUserRequest { user_id, email: user.email, display_name: display_name.clone(), first_name: user.first_name, last_name: user.last_name, avatar, }) .instrument(span) .await?; Ok(Success::new()) } async fn update_group( context: &Context, group: UpdateGroupInput, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] update_group"); span.in_scope(|| { debug!(?group.id); }); if !context.validation_result.is_admin() { span.in_scope(|| debug!("Unauthorized")); return Err("Unauthorized group update".into()); } if group.id == 1 { span.in_scope(|| debug!("Cannot change admin group details")); return Err("Cannot change admin group details".into()); } context .handler .update_group(UpdateGroupRequest { group_id: GroupId(group.id), display_name: group.display_name, }) .instrument(span) .await?; Ok(Success::new()) } async fn add_user_to_group( context: &Context, user_id: String, group_id: i32, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] add_user_to_group"); span.in_scope(|| { debug!(?user_id, ?group_id); }); if !context.validation_result.is_admin() { span.in_scope(|| debug!("Unauthorized")); return Err("Unauthorized group membership modification".into()); } context .handler .add_user_to_group(&UserId::new(&user_id), GroupId(group_id)) .instrument(span) .await?; Ok(Success::new()) } async fn remove_user_from_group( context: &Context, user_id: String, group_id: i32, ) -> FieldResult { let span = debug_span!("[GraphQL mutation] remove_user_from_group"); span.in_scope(|| { debug!(?user_id, ?group_id); }); if !context.validation_result.is_admin() { span.in_scope(|| debug!("Unauthorized")); return Err("Unauthorized group membership modification".into()); } let user_id = UserId::new(&user_id); if context.validation_result.user == user_id && group_id == 1 { span.in_scope(|| debug!("Cannot remove admin rights for current user")); return Err("Cannot remove admin rights for current user".into()); } context .handler .remove_user_from_group(&user_id, GroupId(group_id)) .instrument(span) .await?; Ok(Success::new()) } async fn delete_user(context: &Context, user_id: String) -> FieldResult { let span = debug_span!("[GraphQL mutation] delete_user"); span.in_scope(|| { debug!(?user_id); }); let user_id = UserId::new(&user_id); if !context.validation_result.is_admin() { span.in_scope(|| debug!("Unauthorized")); return Err("Unauthorized user deletion".into()); } if context.validation_result.user == user_id { span.in_scope(|| debug!("Cannot delete current user")); return Err("Cannot delete current user".into()); } context .handler .delete_user(&user_id) .instrument(span) .await?; Ok(Success::new()) } async fn delete_group(context: &Context, group_id: i32) -> FieldResult { let span = debug_span!("[GraphQL mutation] delete_group"); span.in_scope(|| { debug!(?group_id); }); if !context.validation_result.is_admin() { span.in_scope(|| debug!("Unauthorized")); return Err("Unauthorized group deletion".into()); } if group_id == 1 { span.in_scope(|| debug!("Cannot delete admin group")); return Err("Cannot delete admin group".into()); } context .handler .delete_group(GroupId(group_id)) .instrument(span) .await?; Ok(Success::new()) } }