You've already forked authentication-service
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:
@ -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>()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user