You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-09 04:22:45 +03:00
Add a setPassword
GraphQL mutation for setting a user's password (#2820)
* Feed `PasswordManager` through to the GraphQL `State` * Add `setPassword` GraphQL mutation to update a user's password
This commit is contained in:
@@ -200,6 +200,7 @@ impl Options {
|
|||||||
&policy_factory,
|
&policy_factory,
|
||||||
homeserver_connection.clone(),
|
homeserver_connection.clone(),
|
||||||
site_config.clone(),
|
site_config.clone(),
|
||||||
|
password_manager.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let state = {
|
let state = {
|
||||||
|
@@ -59,7 +59,7 @@ use self::{
|
|||||||
mutations::Mutation,
|
mutations::Mutation,
|
||||||
query::Query,
|
query::Query,
|
||||||
};
|
};
|
||||||
use crate::{impl_from_error_for_route, BoundActivityTracker};
|
use crate::{impl_from_error_for_route, passwords::PasswordManager, BoundActivityTracker};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -69,6 +69,7 @@ struct GraphQLState {
|
|||||||
homeserver_connection: Arc<dyn HomeserverConnection<Error = anyhow::Error>>,
|
homeserver_connection: Arc<dyn HomeserverConnection<Error = anyhow::Error>>,
|
||||||
policy_factory: Arc<PolicyFactory>,
|
policy_factory: Arc<PolicyFactory>,
|
||||||
site_config: SiteConfig,
|
site_config: SiteConfig,
|
||||||
|
password_manager: PasswordManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -85,6 +86,10 @@ impl state::State for GraphQLState {
|
|||||||
self.policy_factory.instantiate().await
|
self.policy_factory.instantiate().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn password_manager(&self) -> PasswordManager {
|
||||||
|
self.password_manager.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn site_config(&self) -> &SiteConfig {
|
fn site_config(&self) -> &SiteConfig {
|
||||||
&self.site_config
|
&self.site_config
|
||||||
}
|
}
|
||||||
@@ -113,12 +118,14 @@ pub fn schema(
|
|||||||
policy_factory: &Arc<PolicyFactory>,
|
policy_factory: &Arc<PolicyFactory>,
|
||||||
homeserver_connection: impl HomeserverConnection<Error = anyhow::Error> + 'static,
|
homeserver_connection: impl HomeserverConnection<Error = anyhow::Error> + 'static,
|
||||||
site_config: SiteConfig,
|
site_config: SiteConfig,
|
||||||
|
password_manager: PasswordManager,
|
||||||
) -> Schema {
|
) -> Schema {
|
||||||
let state = GraphQLState {
|
let state = GraphQLState {
|
||||||
pool: pool.clone(),
|
pool: pool.clone(),
|
||||||
policy_factory: Arc::clone(policy_factory),
|
policy_factory: Arc::clone(policy_factory),
|
||||||
homeserver_connection: Arc::new(homeserver_connection),
|
homeserver_connection: Arc::new(homeserver_connection),
|
||||||
site_config,
|
site_config,
|
||||||
|
password_manager,
|
||||||
};
|
};
|
||||||
let state: BoxState = Box::new(state);
|
let state: BoxState = Box::new(state);
|
||||||
|
|
||||||
|
@@ -19,6 +19,7 @@ use mas_storage::{
|
|||||||
user::UserRepository,
|
user::UserRepository,
|
||||||
};
|
};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
use crate::graphql::{
|
use crate::graphql::{
|
||||||
model::{NodeType, User},
|
model::{NodeType, User},
|
||||||
@@ -199,6 +200,66 @@ impl AllowUserCrossSigningResetPayload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The input for the `setPassword` mutation.
|
||||||
|
#[derive(InputObject)]
|
||||||
|
struct SetPasswordInput {
|
||||||
|
/// The ID of the user to set the password for.
|
||||||
|
/// If you are not a server administrator then this must be your own user
|
||||||
|
/// ID.
|
||||||
|
user_id: ID,
|
||||||
|
|
||||||
|
/// The current password of the user.
|
||||||
|
/// Required if you are not a server administrator.
|
||||||
|
current_password: Option<String>,
|
||||||
|
|
||||||
|
/// The new password for the user.
|
||||||
|
new_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The return type for the `setPassword` mutation.
|
||||||
|
#[derive(Description)]
|
||||||
|
struct SetPasswordPayload {
|
||||||
|
status: SetPasswordStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The status of the `setPassword` mutation.
|
||||||
|
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
|
||||||
|
enum SetPasswordStatus {
|
||||||
|
/// The password was updated.
|
||||||
|
Allowed,
|
||||||
|
|
||||||
|
/// The user was not found.
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
/// The user doesn't have a current password to attempt to match against.
|
||||||
|
NoCurrentPassword,
|
||||||
|
|
||||||
|
/// The supplied current password was wrong.
|
||||||
|
WrongPassword,
|
||||||
|
|
||||||
|
/// The new password is invalid. For example, it may not meet configured
|
||||||
|
/// security requirements.
|
||||||
|
InvalidNewPassword,
|
||||||
|
|
||||||
|
/// You aren't allowed to set the password for that user.
|
||||||
|
/// This happens if you aren't setting your own password and you aren't a
|
||||||
|
/// server administrator.
|
||||||
|
NotAllowed,
|
||||||
|
|
||||||
|
/// Password support has been disabled.
|
||||||
|
/// This usually means that login is handled by an upstream identity
|
||||||
|
/// provider.
|
||||||
|
PasswordChangesDisabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Object(use_type_description)]
|
||||||
|
impl SetPasswordPayload {
|
||||||
|
/// Status of the operation
|
||||||
|
async fn status(&self) -> SetPasswordStatus {
|
||||||
|
self.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn valid_username_character(c: char) -> bool {
|
fn valid_username_character(c: char) -> bool {
|
||||||
c.is_ascii_lowercase()
|
c.is_ascii_lowercase()
|
||||||
|| c.is_ascii_digit()
|
|| c.is_ascii_digit()
|
||||||
@@ -385,4 +446,108 @@ impl UserMutations {
|
|||||||
|
|
||||||
Ok(AllowUserCrossSigningResetPayload::Allowed(user))
|
Ok(AllowUserCrossSigningResetPayload::Allowed(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the password for a user.
|
||||||
|
///
|
||||||
|
/// This can be used by server administrators to set any user's password,
|
||||||
|
/// or, provided the capability hasn't been disabled on this server,
|
||||||
|
/// by a user to change their own password as long as they know their
|
||||||
|
/// current password.
|
||||||
|
async fn set_password(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
input: SetPasswordInput,
|
||||||
|
) -> Result<SetPasswordPayload, async_graphql::Error> {
|
||||||
|
let state = ctx.state();
|
||||||
|
let user_id = NodeType::User.extract_ulid(&input.user_id)?;
|
||||||
|
let requester = ctx.requester();
|
||||||
|
|
||||||
|
if !requester.is_owner_or_admin(&UserId(user_id)) {
|
||||||
|
return Err(async_graphql::Error::new("Unauthorized"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut policy = state.policy().await?;
|
||||||
|
|
||||||
|
let res = policy.evaluate_password(&input.new_password).await?;
|
||||||
|
|
||||||
|
if !res.valid() {
|
||||||
|
// TODO Expose the reason for the policy violation
|
||||||
|
// This involves redesigning the error handling
|
||||||
|
// Idea would be to expose an errors array in the response,
|
||||||
|
// with a list of union of different error kinds.
|
||||||
|
return Ok(SetPasswordPayload {
|
||||||
|
status: SetPasswordStatus::InvalidNewPassword,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut repo = state.repository().await?;
|
||||||
|
let Some(user) = repo.user().lookup(user_id).await? else {
|
||||||
|
return Ok(SetPasswordPayload {
|
||||||
|
status: SetPasswordStatus::NotFound,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let password_manager = state.password_manager();
|
||||||
|
if !requester.is_admin() {
|
||||||
|
// If the user isn't an admin, we:
|
||||||
|
// - check that password changes are enabled
|
||||||
|
// - check that they know their current password
|
||||||
|
|
||||||
|
if !state.site_config().password_change_allowed || !password_manager.is_enabled() {
|
||||||
|
return Ok(SetPasswordPayload {
|
||||||
|
status: SetPasswordStatus::PasswordChangesDisabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(active_password) = repo.user_password().active(&user).await? else {
|
||||||
|
// The user has no current password, so can't verify against one.
|
||||||
|
// In the future, it may be desirable to let the user set a password without any
|
||||||
|
// other verification instead.
|
||||||
|
|
||||||
|
return Ok(SetPasswordPayload {
|
||||||
|
status: SetPasswordStatus::NoCurrentPassword,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(current_password_attempt) = input.current_password else {
|
||||||
|
return Err(async_graphql::Error::new(
|
||||||
|
"You must supply `currentPassword` to change your own password if you are not an administrator"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(_err) = password_manager
|
||||||
|
.verify(
|
||||||
|
active_password.version,
|
||||||
|
Zeroizing::new(current_password_attempt.into_bytes()),
|
||||||
|
active_password.hashed_password,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Ok(SetPasswordPayload {
|
||||||
|
status: SetPasswordStatus::WrongPassword,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (new_password_version, new_password_hash) = password_manager
|
||||||
|
.hash(state.rng(), Zeroizing::new(input.new_password.into_bytes()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
repo.user_password()
|
||||||
|
.add(
|
||||||
|
&mut state.rng(),
|
||||||
|
&state.clock(),
|
||||||
|
&user,
|
||||||
|
new_password_version,
|
||||||
|
new_password_hash,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
repo.save().await?;
|
||||||
|
|
||||||
|
Ok(SetPasswordPayload {
|
||||||
|
status: SetPasswordStatus::Allowed,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,12 +17,13 @@ use mas_matrix::HomeserverConnection;
|
|||||||
use mas_policy::Policy;
|
use mas_policy::Policy;
|
||||||
use mas_storage::{BoxClock, BoxRepository, BoxRng, RepositoryError};
|
use mas_storage::{BoxClock, BoxRepository, BoxRng, RepositoryError};
|
||||||
|
|
||||||
use crate::graphql::Requester;
|
use crate::{graphql::Requester, passwords::PasswordManager};
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait State {
|
pub trait State {
|
||||||
async fn repository(&self) -> Result<BoxRepository, RepositoryError>;
|
async fn repository(&self) -> Result<BoxRepository, RepositoryError>;
|
||||||
async fn policy(&self) -> Result<Policy, mas_policy::InstantiateError>;
|
async fn policy(&self) -> Result<Policy, mas_policy::InstantiateError>;
|
||||||
|
fn password_manager(&self) -> PasswordManager;
|
||||||
fn homeserver_connection(&self) -> &dyn HomeserverConnection<Error = anyhow::Error>;
|
fn homeserver_connection(&self) -> &dyn HomeserverConnection<Error = anyhow::Error>;
|
||||||
fn clock(&self) -> BoxClock;
|
fn clock(&self) -> BoxClock;
|
||||||
fn rng(&self) -> BoxRng;
|
fn rng(&self) -> BoxRng;
|
||||||
|
@@ -198,6 +198,7 @@ impl TestState {
|
|||||||
site_config: site_config.clone(),
|
site_config: site_config.clone(),
|
||||||
rng: Arc::clone(&rng),
|
rng: Arc::clone(&rng),
|
||||||
clock: Arc::clone(&clock),
|
clock: Arc::clone(&clock),
|
||||||
|
password_manager: password_manager.clone(),
|
||||||
};
|
};
|
||||||
let state: crate::graphql::BoxState = Box::new(graphql_state);
|
let state: crate::graphql::BoxState = Box::new(graphql_state);
|
||||||
|
|
||||||
@@ -314,6 +315,7 @@ struct TestGraphQLState {
|
|||||||
policy_factory: Arc<PolicyFactory>,
|
policy_factory: Arc<PolicyFactory>,
|
||||||
clock: Arc<MockClock>,
|
clock: Arc<MockClock>,
|
||||||
rng: Arc<Mutex<ChaChaRng>>,
|
rng: Arc<Mutex<ChaChaRng>>,
|
||||||
|
password_manager: PasswordManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -332,6 +334,10 @@ impl graphql::State for TestGraphQLState {
|
|||||||
self.policy_factory.instantiate().await
|
self.policy_factory.instantiate().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn password_manager(&self) -> PasswordManager {
|
||||||
|
self.password_manager.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn homeserver_connection(&self) -> &dyn HomeserverConnection<Error = anyhow::Error> {
|
fn homeserver_connection(&self) -> &dyn HomeserverConnection<Error = anyhow::Error> {
|
||||||
&self.homeserver_connection
|
&self.homeserver_connection
|
||||||
}
|
}
|
||||||
|
@@ -748,6 +748,15 @@ type Mutation {
|
|||||||
input: AllowUserCrossSigningResetInput!
|
input: AllowUserCrossSigningResetInput!
|
||||||
): AllowUserCrossSigningResetPayload!
|
): AllowUserCrossSigningResetPayload!
|
||||||
"""
|
"""
|
||||||
|
Set the password for a user.
|
||||||
|
|
||||||
|
This can be used by server administrators to set any user's password,
|
||||||
|
or, provided the capability hasn't been disabled on this server,
|
||||||
|
by a user to change their own password as long as they know their
|
||||||
|
current password.
|
||||||
|
"""
|
||||||
|
setPassword(input: SetPasswordInput!): SetPasswordPayload!
|
||||||
|
"""
|
||||||
Create a new arbitrary OAuth 2.0 Session.
|
Create a new arbitrary OAuth 2.0 Session.
|
||||||
|
|
||||||
Only available for administrators.
|
Only available for administrators.
|
||||||
@@ -1205,6 +1214,76 @@ enum SetDisplayNameStatus {
|
|||||||
INVALID
|
INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The input for the `setPassword` mutation.
|
||||||
|
"""
|
||||||
|
input SetPasswordInput {
|
||||||
|
"""
|
||||||
|
The ID of the user to set the password for.
|
||||||
|
If you are not a server administrator then this must be your own user
|
||||||
|
ID.
|
||||||
|
"""
|
||||||
|
userId: ID!
|
||||||
|
"""
|
||||||
|
The current password of the user.
|
||||||
|
Required if you are not a server administrator.
|
||||||
|
"""
|
||||||
|
currentPassword: String
|
||||||
|
"""
|
||||||
|
The new password for the user.
|
||||||
|
"""
|
||||||
|
newPassword: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The return type for the `setPassword` mutation.
|
||||||
|
"""
|
||||||
|
type SetPasswordPayload {
|
||||||
|
"""
|
||||||
|
Status of the operation
|
||||||
|
"""
|
||||||
|
status: SetPasswordStatus!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The status of the `setPassword` mutation.
|
||||||
|
"""
|
||||||
|
enum SetPasswordStatus {
|
||||||
|
"""
|
||||||
|
The password was updated.
|
||||||
|
"""
|
||||||
|
ALLOWED
|
||||||
|
"""
|
||||||
|
The user was not found.
|
||||||
|
"""
|
||||||
|
NOT_FOUND
|
||||||
|
"""
|
||||||
|
The user doesn't have a current password to attempt to match against.
|
||||||
|
"""
|
||||||
|
NO_CURRENT_PASSWORD
|
||||||
|
"""
|
||||||
|
The supplied current password was wrong.
|
||||||
|
"""
|
||||||
|
WRONG_PASSWORD
|
||||||
|
"""
|
||||||
|
The new password is invalid. For example, it may not meet configured
|
||||||
|
security requirements.
|
||||||
|
"""
|
||||||
|
INVALID_NEW_PASSWORD
|
||||||
|
"""
|
||||||
|
You aren't allowed to set the password for that user.
|
||||||
|
This happens if you aren't setting your own password and you aren't a
|
||||||
|
server administrator.
|
||||||
|
"""
|
||||||
|
NOT_ALLOWED
|
||||||
|
"""
|
||||||
|
Password support has been disabled.
|
||||||
|
This usually means that login is handled by an upstream identity
|
||||||
|
provider.
|
||||||
|
"""
|
||||||
|
PASSWORD_CHANGES_DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The input for the `setPrimaryEmail` mutation
|
The input for the `setPrimaryEmail` mutation
|
||||||
"""
|
"""
|
||||||
|
@@ -486,6 +486,15 @@ export type Mutation = {
|
|||||||
setCanRequestAdmin: SetCanRequestAdminPayload;
|
setCanRequestAdmin: SetCanRequestAdminPayload;
|
||||||
/** Set the display name of a user */
|
/** Set the display name of a user */
|
||||||
setDisplayName: SetDisplayNamePayload;
|
setDisplayName: SetDisplayNamePayload;
|
||||||
|
/**
|
||||||
|
* Set the password for a user.
|
||||||
|
*
|
||||||
|
* This can be used by server administrators to set any user's password,
|
||||||
|
* or, provided the capability hasn't been disabled on this server,
|
||||||
|
* by a user to change their own password as long as they know their
|
||||||
|
* current password.
|
||||||
|
*/
|
||||||
|
setPassword: SetPasswordPayload;
|
||||||
/** Set an email address as primary */
|
/** Set an email address as primary */
|
||||||
setPrimaryEmail: SetPrimaryEmailPayload;
|
setPrimaryEmail: SetPrimaryEmailPayload;
|
||||||
/** Submit a verification code for an email address */
|
/** Submit a verification code for an email address */
|
||||||
@@ -565,6 +574,12 @@ export type MutationSetDisplayNameArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/** The mutations root of the GraphQL interface. */
|
||||||
|
export type MutationSetPasswordArgs = {
|
||||||
|
input: SetPasswordInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/** The mutations root of the GraphQL interface. */
|
/** The mutations root of the GraphQL interface. */
|
||||||
export type MutationSetPrimaryEmailArgs = {
|
export type MutationSetPrimaryEmailArgs = {
|
||||||
input: SetPrimaryEmailInput;
|
input: SetPrimaryEmailInput;
|
||||||
@@ -903,6 +918,59 @@ export enum SetDisplayNameStatus {
|
|||||||
Set = 'SET'
|
Set = 'SET'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The input for the `setPassword` mutation. */
|
||||||
|
export type SetPasswordInput = {
|
||||||
|
/**
|
||||||
|
* The current password of the user.
|
||||||
|
* Required if you are not a server administrator.
|
||||||
|
*/
|
||||||
|
currentPassword?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
/** The new password for the user. */
|
||||||
|
newPassword: Scalars['String']['input'];
|
||||||
|
/**
|
||||||
|
* The ID of the user to set the password for.
|
||||||
|
* If you are not a server administrator then this must be your own user
|
||||||
|
* ID.
|
||||||
|
*/
|
||||||
|
userId: Scalars['ID']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The return type for the `setPassword` mutation. */
|
||||||
|
export type SetPasswordPayload = {
|
||||||
|
__typename?: 'SetPasswordPayload';
|
||||||
|
/** Status of the operation */
|
||||||
|
status: SetPasswordStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The status of the `setPassword` mutation. */
|
||||||
|
export enum SetPasswordStatus {
|
||||||
|
/** The password was updated. */
|
||||||
|
Allowed = 'ALLOWED',
|
||||||
|
/**
|
||||||
|
* The new password is invalid. For example, it may not meet configured
|
||||||
|
* security requirements.
|
||||||
|
*/
|
||||||
|
InvalidNewPassword = 'INVALID_NEW_PASSWORD',
|
||||||
|
/**
|
||||||
|
* You aren't allowed to set the password for that user.
|
||||||
|
* This happens if you aren't setting your own password and you aren't a
|
||||||
|
* server administrator.
|
||||||
|
*/
|
||||||
|
NotAllowed = 'NOT_ALLOWED',
|
||||||
|
/** The user was not found. */
|
||||||
|
NotFound = 'NOT_FOUND',
|
||||||
|
/** The user doesn't have a current password to attempt to match against. */
|
||||||
|
NoCurrentPassword = 'NO_CURRENT_PASSWORD',
|
||||||
|
/**
|
||||||
|
* Password support has been disabled.
|
||||||
|
* This usually means that login is handled by an upstream identity
|
||||||
|
* provider.
|
||||||
|
*/
|
||||||
|
PasswordChangesDisabled = 'PASSWORD_CHANGES_DISABLED',
|
||||||
|
/** The supplied current password was wrong. */
|
||||||
|
WrongPassword = 'WRONG_PASSWORD'
|
||||||
|
}
|
||||||
|
|
||||||
/** The input for the `setPrimaryEmail` mutation */
|
/** The input for the `setPrimaryEmail` mutation */
|
||||||
export type SetPrimaryEmailInput = {
|
export type SetPrimaryEmailInput = {
|
||||||
/** The ID of the email address to set as primary */
|
/** The ID of the email address to set as primary */
|
||||||
|
@@ -1422,6 +1422,29 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "setPassword",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"ofType": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "SetPasswordPayload",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "input",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "setPrimaryEmail",
|
"name": "setPrimaryEmail",
|
||||||
"type": {
|
"type": {
|
||||||
@@ -2386,6 +2409,24 @@ export default {
|
|||||||
],
|
],
|
||||||
"interfaces": []
|
"interfaces": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "SetPasswordPayload",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "Any"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"interfaces": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "SetPrimaryEmailPayload",
|
"name": "SetPrimaryEmailPayload",
|
||||||
|
Reference in New Issue
Block a user