Skip to content

Commit 1f03d6d

Browse files
committed
GraphQL mutation to deactivate a user
1 parent 19f1091 commit 1f03d6d

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

crates/handlers/src/graphql/mutations/user.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use ulid::Ulid;
1818
use url::Url;
1919
use zeroize::Zeroizing;
2020

21+
use super::verify_password_if_needed;
2122
use crate::graphql::{
2223
UserId,
2324
model::{NodeType, User},
@@ -383,6 +384,54 @@ impl ResendRecoveryEmailPayload {
383384
}
384385
}
385386

387+
/// The input for the `deactivateUser` mutation.
388+
#[derive(InputObject)]
389+
pub struct DeactivateUserInput {
390+
/// Whether to ask the homeserver to GDPR-erase the user
391+
hs_erase: bool,
392+
393+
/// The password of the user to deactivate.
394+
password: Option<String>,
395+
}
396+
397+
/// The payload for the `deactivateUser` mutation.
398+
#[derive(Description)]
399+
pub enum DeactivateUserPayload {
400+
/// The user was deactivated.
401+
Deactivated(mas_data_model::User),
402+
403+
/// The password was wrong or missing.
404+
IncorrectPassword,
405+
}
406+
407+
/// The status of the `deactivateUser` mutation.
408+
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
409+
pub enum DeactivateUserStatus {
410+
/// The user was deactivated.
411+
Deactivated,
412+
413+
/// The password was wrong.
414+
IncorrectPassword,
415+
}
416+
417+
#[Object(use_type_description)]
418+
impl DeactivateUserPayload {
419+
/// Status of the operation
420+
async fn status(&self) -> DeactivateUserStatus {
421+
match self {
422+
Self::Deactivated(_) => DeactivateUserStatus::Deactivated,
423+
Self::IncorrectPassword => DeactivateUserStatus::IncorrectPassword,
424+
}
425+
}
426+
427+
async fn user(&self) -> Option<User> {
428+
match self {
429+
Self::Deactivated(user) => Some(User(user.clone())),
430+
Self::IncorrectPassword => None,
431+
}
432+
}
433+
}
434+
386435
fn valid_username_character(c: char) -> bool {
387436
c.is_ascii_lowercase()
388437
|| c.is_ascii_digit()
@@ -868,4 +917,64 @@ impl UserMutations {
868917
recovery_session_id: recovery_session.id,
869918
})
870919
}
920+
921+
/// Deactivate the current user account
922+
///
923+
/// If the user has a password, it *must* be supplied in the `password`
924+
/// field.
925+
async fn deactivate_user(
926+
&self,
927+
ctx: &Context<'_>,
928+
input: DeactivateUserInput,
929+
) -> Result<DeactivateUserPayload, async_graphql::Error> {
930+
let state = ctx.state();
931+
let mut rng = state.rng();
932+
let clock = state.clock();
933+
let requester = ctx.requester();
934+
let site_config = state.site_config();
935+
936+
// Only allow calling this if the requester is a browser session
937+
let Some(browser_session) = requester.browser_session() else {
938+
return Err(async_graphql::Error::new("Unauthorized"));
939+
};
940+
941+
if !site_config.account_deactivation_allowed {
942+
return Err(async_graphql::Error::new(
943+
"Account deactivation is not allowed on this server",
944+
));
945+
}
946+
947+
let mut repo = state.repository().await?;
948+
if !verify_password_if_needed(
949+
requester,
950+
site_config,
951+
&state.password_manager(),
952+
input.password,
953+
&browser_session.user,
954+
&mut repo,
955+
)
956+
.await?
957+
{
958+
return Ok(DeactivateUserPayload::IncorrectPassword);
959+
}
960+
961+
// Deactivate the user right away
962+
let user = repo
963+
.user()
964+
.deactivate(&state.clock(), browser_session.user.clone())
965+
.await?;
966+
967+
// and then schedule a job to deactivate it fully
968+
repo.queue_job()
969+
.schedule_job(
970+
&mut rng,
971+
&clock,
972+
DeactivateUserJob::new(&user, input.hs_erase),
973+
)
974+
.await?;
975+
976+
repo.save().await?;
977+
978+
Ok(DeactivateUserPayload::Deactivated(user))
979+
}
871980
}

frontend/schema.graphql

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,45 @@ The input/output is a string in RFC3339 format.
601601
"""
602602
scalar DateTime
603603

604+
"""
605+
The input for the `deactivateUser` mutation.
606+
"""
607+
input DeactivateUserInput {
608+
"""
609+
Whether to ask the homeserver to GDPR-erase the user
610+
"""
611+
hsErase: Boolean!
612+
"""
613+
The password of the user to deactivate.
614+
"""
615+
password: String
616+
}
617+
618+
"""
619+
The payload for the `deactivateUser` mutation.
620+
"""
621+
type DeactivateUserPayload {
622+
"""
623+
Status of the operation
624+
"""
625+
status: DeactivateUserStatus!
626+
user: User
627+
}
628+
629+
"""
630+
The status of the `deactivateUser` mutation.
631+
"""
632+
enum DeactivateUserStatus {
633+
"""
634+
The user was deactivated.
635+
"""
636+
DEACTIVATED
637+
"""
638+
The password was wrong.
639+
"""
640+
INCORRECT_PASSWORD
641+
}
642+
604643
"""
605644
The type of a user agent
606645
"""
@@ -876,6 +915,13 @@ type Mutation {
876915
input: ResendRecoveryEmailInput!
877916
): ResendRecoveryEmailPayload!
878917
"""
918+
Deactivate the current user account
919+
920+
If the user has a password, it *must* be supplied in the `password`
921+
field.
922+
"""
923+
deactivateUser(input: DeactivateUserInput!): DeactivateUserPayload!
924+
"""
879925
Create a new arbitrary OAuth 2.0 Session.
880926
881927
Only available for administrators.

frontend/src/gql/graphql.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,29 @@ export type DateFilter = {
389389
before?: InputMaybe<Scalars['DateTime']['input']>;
390390
};
391391

392+
/** The input for the `deactivateUser` mutation. */
393+
export type DeactivateUserInput = {
394+
/** Whether to ask the homeserver to GDPR-erase the user */
395+
hsErase: Scalars['Boolean']['input'];
396+
/** The password of the user to deactivate. */
397+
password?: InputMaybe<Scalars['String']['input']>;
398+
};
399+
400+
/** The payload for the `deactivateUser` mutation. */
401+
export type DeactivateUserPayload = {
402+
__typename?: 'DeactivateUserPayload';
403+
/** Status of the operation */
404+
status: DeactivateUserStatus;
405+
user?: Maybe<User>;
406+
};
407+
408+
/** The status of the `deactivateUser` mutation. */
409+
export type DeactivateUserStatus =
410+
/** The user was deactivated. */
411+
| 'DEACTIVATED'
412+
/** The password was wrong. */
413+
| 'INCORRECT_PASSWORD';
414+
392415
/** The type of a user agent */
393416
export type DeviceType =
394417
/** A mobile phone. Can also sometimes be a tablet. */
@@ -519,6 +542,13 @@ export type Mutation = {
519542
* Only available for administrators.
520543
*/
521544
createOauth2Session: CreateOAuth2SessionPayload;
545+
/**
546+
* Deactivate the current user account
547+
*
548+
* If the user has a password, it *must* be supplied in the `password`
549+
* field.
550+
*/
551+
deactivateUser: DeactivateUserPayload;
522552
endBrowserSession: EndBrowserSessionPayload;
523553
endCompatSession: EndCompatSessionPayload;
524554
endOauth2Session: EndOAuth2SessionPayload;
@@ -596,6 +626,12 @@ export type MutationCreateOauth2SessionArgs = {
596626
};
597627

598628

629+
/** The mutations root of the GraphQL interface. */
630+
export type MutationDeactivateUserArgs = {
631+
input: DeactivateUserInput;
632+
};
633+
634+
599635
/** The mutations root of the GraphQL interface. */
600636
export type MutationEndBrowserSessionArgs = {
601637
input: EndBrowserSessionInput;

0 commit comments

Comments
 (0)