1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

Have better output types on mutations

This commit is contained in:
Quentin Gliech
2023-04-25 16:12:20 +02:00
parent 3b0f2ea1b0
commit ed5c367df6
8 changed files with 647 additions and 173 deletions

View File

@ -36,17 +36,17 @@ mod state;
pub use self::{
model::{CreationEvent, Node},
mutations::RootMutations,
query::RootQuery,
mutations::Mutation,
query::Query,
state::{BoxState, State},
};
pub type Schema = async_graphql::Schema<RootQuery, RootMutations, EmptySubscription>;
pub type SchemaBuilder = async_graphql::SchemaBuilder<RootQuery, RootMutations, EmptySubscription>;
pub type Schema = async_graphql::Schema<Query, Mutation, EmptySubscription>;
pub type SchemaBuilder = async_graphql::SchemaBuilder<Query, Mutation, EmptySubscription>;
#[must_use]
pub fn schema_builder() -> SchemaBuilder {
async_graphql::Schema::build(RootQuery::new(), RootMutations::new(), EmptySubscription)
async_graphql::Schema::build(Query::new(), Mutation::new(), EmptySubscription)
.register_output_type::<Node>()
.register_output_type::<CreationEvent>()
}

View File

@ -18,9 +18,9 @@ use async_graphql::MergedObject;
/// The mutations root of the GraphQL interface.
#[derive(Default, MergedObject)]
pub struct RootMutations(user_email::UserEmailMutations);
pub struct Mutation(user_email::UserEmailMutations);
impl RootMutations {
impl Mutation {
#[must_use]
pub fn new() -> Self {
Self::default()

View File

@ -13,11 +13,15 @@
// limitations under the License.
use anyhow::Context as _;
use async_graphql::{Context, InputObject, Object, ID};
use mas_storage::job::{JobRepositoryExt, ProvisionUserJob, VerifyEmailJob};
use async_graphql::{Context, Description, Enum, InputObject, Object, ID};
use mas_storage::{
job::{JobRepositoryExt, ProvisionUserJob, VerifyEmailJob},
user::UserRepository,
RepositoryAccess,
};
use crate::{
model::{NodeType, UserEmail},
model::{NodeType, User, UserEmail},
state::ContextExt,
};
@ -35,6 +39,60 @@ struct AddEmailInput {
user_id: ID,
}
/// The status of the `addEmail` mutation
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum AddEmailStatus {
/// The email address was added
Added,
/// The email address already exists
Exists,
}
/// The payload of the `addEmail` mutation
#[derive(Description)]
enum AddEmailPayload {
Added(mas_data_model::UserEmail),
Exists(mas_data_model::UserEmail),
}
#[Object(use_type_description)]
impl AddEmailPayload {
/// Status of the operation
async fn status(&self) -> AddEmailStatus {
match self {
AddEmailPayload::Added(_) => AddEmailStatus::Added,
AddEmailPayload::Exists(_) => AddEmailStatus::Exists,
}
}
/// The email address that was added
async fn email(&self) -> UserEmail {
match self {
AddEmailPayload::Added(email) | AddEmailPayload::Exists(email) => {
UserEmail(email.clone())
}
}
}
/// The user to whom the email address was added
async fn user(&self, ctx: &Context<'_>) -> Result<User, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let user_id = match self {
AddEmailPayload::Added(email) | AddEmailPayload::Exists(email) => email.user_id,
};
let user = repo
.user()
.lookup(user_id)
.await?
.context("User not found")?;
Ok(User(user))
}
}
/// The input for the `sendVerificationEmail` mutation
#[derive(InputObject)]
struct SendVerificationEmailInput {
@ -42,6 +100,62 @@ struct SendVerificationEmailInput {
user_email_id: ID,
}
/// The status of the `sendVerificationEmail` mutation
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum SendVerificationEmailStatus {
/// The verification email was sent
Sent,
/// The email address is already verified
AlreadyVerified,
}
/// The payload of the `sendVerificationEmail` mutation
#[derive(Description)]
enum SendVerificationEmailPayload {
Sent(mas_data_model::UserEmail),
AlreadyVerified(mas_data_model::UserEmail),
}
#[Object(use_type_description)]
impl SendVerificationEmailPayload {
/// Status of the operation
async fn status(&self) -> SendVerificationEmailStatus {
match self {
SendVerificationEmailPayload::Sent(_) => SendVerificationEmailStatus::Sent,
SendVerificationEmailPayload::AlreadyVerified(_) => {
SendVerificationEmailStatus::AlreadyVerified
}
}
}
/// The email address to which the verification email was sent
async fn email(&self) -> UserEmail {
match self {
SendVerificationEmailPayload::Sent(email)
| SendVerificationEmailPayload::AlreadyVerified(email) => UserEmail(email.clone()),
}
}
/// The user to whom the email address belongs
async fn user(&self, ctx: &Context<'_>) -> Result<User, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let user_id = match self {
SendVerificationEmailPayload::Sent(email)
| SendVerificationEmailPayload::AlreadyVerified(email) => email.user_id,
};
let user = repo
.user()
.lookup(user_id)
.await?
.context("User not found")?;
Ok(User(user))
}
}
/// The input for the `verifyEmail` mutation
#[derive(InputObject)]
struct VerifyEmailInput {
@ -51,6 +165,68 @@ struct VerifyEmailInput {
code: String,
}
/// The status of the `verifyEmail` mutation
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum VerifyEmailStatus {
/// The email address was just verified
Verified,
/// The email address was already verified before
AlreadyVerified,
/// The verification code is invalid
InvalidCode,
}
/// The payload of the `verifyEmail` mutation
#[derive(Description)]
enum VerifyEmailPayload {
Verified(mas_data_model::UserEmail),
AlreadyVerified(mas_data_model::UserEmail),
InvalidCode,
}
#[Object(use_type_description)]
impl VerifyEmailPayload {
/// Status of the operation
async fn status(&self) -> VerifyEmailStatus {
match self {
VerifyEmailPayload::Verified(_) => VerifyEmailStatus::Verified,
VerifyEmailPayload::AlreadyVerified(_) => VerifyEmailStatus::AlreadyVerified,
VerifyEmailPayload::InvalidCode => VerifyEmailStatus::InvalidCode,
}
}
/// The email address that was verified
async fn email(&self) -> Option<UserEmail> {
match self {
VerifyEmailPayload::Verified(email) | VerifyEmailPayload::AlreadyVerified(email) => {
Some(UserEmail(email.clone()))
}
VerifyEmailPayload::InvalidCode => None,
}
}
/// The user to whom the email address belongs
async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let user_id = match self {
VerifyEmailPayload::Verified(email) | VerifyEmailPayload::AlreadyVerified(email) => {
email.user_id
}
VerifyEmailPayload::InvalidCode => return Ok(None),
};
let user = repo
.user()
.lookup(user_id)
.await?
.context("User not found")?;
Ok(Some(User(user)))
}
}
#[Object]
impl UserEmailMutations {
/// Add an email address to the specified user
@ -58,7 +234,7 @@ impl UserEmailMutations {
&self,
ctx: &Context<'_>,
input: AddEmailInput,
) -> Result<UserEmail, async_graphql::Error> {
) -> Result<AddEmailPayload, async_graphql::Error> {
let state = ctx.state();
let id = NodeType::User.extract_ulid(&input.user_id)?;
let requester = ctx.requester();
@ -75,15 +251,18 @@ impl UserEmailMutations {
// duplicated in mas_handlers
// Find an existing email address
let existing_user_email = repo.user_email().find(user, &input.email).await?;
let user_email = if let Some(user_email) = existing_user_email {
user_email
let (added, user_email) = if let Some(user_email) = existing_user_email {
(false, user_email)
} else {
let clock = state.clock();
let mut rng = state.rng();
repo.user_email()
let user_email = repo
.user_email()
.add(&mut rng, &clock, user, input.email)
.await?
.await?;
(true, user_email)
};
// Schedule a job to verify the email address if needed
@ -95,7 +274,12 @@ impl UserEmailMutations {
repo.save().await?;
Ok(UserEmail(user_email))
let payload = if added {
AddEmailPayload::Added(user_email)
} else {
AddEmailPayload::Exists(user_email)
};
Ok(payload)
}
/// Send a verification code for an email address
@ -103,7 +287,7 @@ impl UserEmailMutations {
&self,
ctx: &Context<'_>,
input: SendVerificationEmailInput,
) -> Result<UserEmail, async_graphql::Error> {
) -> Result<SendVerificationEmailPayload, async_graphql::Error> {
let state = ctx.state();
let user_email_id = NodeType::UserEmail.extract_ulid(&input.user_email_id)?;
let requester = ctx.requester();
@ -122,7 +306,8 @@ impl UserEmailMutations {
}
// Schedule a job to verify the email address if needed
if user_email.confirmed_at.is_none() {
let needs_verification = user_email.confirmed_at.is_none();
if needs_verification {
repo.job()
.schedule_job(VerifyEmailJob::new(&user_email))
.await?;
@ -130,7 +315,12 @@ impl UserEmailMutations {
repo.save().await?;
Ok(UserEmail(user_email))
let payload = if needs_verification {
SendVerificationEmailPayload::Sent(user_email)
} else {
SendVerificationEmailPayload::AlreadyVerified(user_email)
};
Ok(payload)
}
/// Submit a verification code for an email address
@ -138,7 +328,7 @@ impl UserEmailMutations {
&self,
ctx: &Context<'_>,
input: VerifyEmailInput,
) -> Result<UserEmail, async_graphql::Error> {
) -> Result<VerifyEmailPayload, async_graphql::Error> {
let state = ctx.state();
let user_email_id = NodeType::UserEmail.extract_ulid(&input.user_email_id)?;
let requester = ctx.requester();
@ -161,7 +351,7 @@ impl UserEmailMutations {
if user_email.confirmed_at.is_some() {
// Just return the email address if it's already verified
// XXX: should we return an error instead?
return Ok(UserEmail(user_email));
return Ok(VerifyEmailPayload::AlreadyVerified(user_email));
}
// XXX: this logic should be extracted somewhere else, since most of it is
@ -172,13 +362,12 @@ impl UserEmailMutations {
.user_email()
.find_verification_code(&clock, &user_email, &input.code)
.await?
.context("Invalid verification code")?;
.filter(|v| v.is_valid());
if verification.is_valid() {
return Err(async_graphql::Error::new("Invalid verification code"));
}
let Some(verification) = verification else {
return Ok(VerifyEmailPayload::InvalidCode);
};
// TODO: display nice errors if the code was already consumed or expired
repo.user_email()
.consume_verification_code(&clock, verification)
.await?;
@ -197,6 +386,6 @@ impl UserEmailMutations {
repo.save().await?;
Ok(UserEmail(user_email))
Ok(VerifyEmailPayload::Verified(user_email))
}
}

View File

@ -26,9 +26,9 @@ use self::{upstream_oauth::UpstreamOAuthQuery, viewer::ViewerQuery};
/// The query root of the GraphQL interface.
#[derive(Default, MergedObject)]
pub struct RootQuery(BaseQuery, UpstreamOAuthQuery, ViewerQuery);
pub struct Query(BaseQuery, UpstreamOAuthQuery, ViewerQuery);
impl RootQuery {
impl Query {
#[must_use]
pub fn new() -> Self {
Self::default()