From e9a57a4ce02803fa05567dab89ee3a1bda95fac9 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 12 Jul 2024 14:47:46 +0200 Subject: [PATCH] GraphQL API to unlock a user Fixes #2101 --- crates/handlers/src/graphql/mutations/user.rs | 80 +++++++++++++++++++ frontend/schema.graphql | 42 ++++++++++ frontend/src/gql/graphql.ts | 31 +++++++ frontend/src/gql/schema.ts | 50 ++++++++++++ 4 files changed, 203 insertions(+) diff --git a/crates/handlers/src/graphql/mutations/user.rs b/crates/handlers/src/graphql/mutations/user.rs index 7568c2ee9..1fa64c6c8 100644 --- a/crates/handlers/src/graphql/mutations/user.rs +++ b/crates/handlers/src/graphql/mutations/user.rs @@ -141,6 +141,52 @@ impl LockUserPayload { } } +/// The input for the `unlockUser` mutation. +#[derive(InputObject)] +struct UnlockUserInput { + /// The ID of the user to unlock + user_id: ID, +} + +/// The status of the `unlockUser` mutation. +#[derive(Enum, Copy, Clone, Eq, PartialEq)] +enum UnlockUserStatus { + /// The user was unlocked. + Unlocked, + + /// The user was not found. + NotFound, +} + +/// The payload for the `unlockUser` mutation. +#[derive(Description)] +enum UnlockUserPayload { + /// The user was unlocked. + Unlocked(mas_data_model::User), + + /// The user was not found. + NotFound, +} + +#[Object(use_type_description)] +impl UnlockUserPayload { + /// Status of the operation + async fn status(&self) -> UnlockUserStatus { + match self { + Self::Unlocked(_) => UnlockUserStatus::Unlocked, + Self::NotFound => UnlockUserStatus::NotFound, + } + } + + /// The user that was unlocked. + async fn user(&self) -> Option { + match self { + Self::Unlocked(user) => Some(User(user.clone())), + Self::NotFound => None, + } + } +} + /// The input for the `setCanRequestAdmin` mutation. #[derive(InputObject)] struct SetCanRequestAdminInput { @@ -382,6 +428,40 @@ impl UserMutations { Ok(LockUserPayload::Locked(user)) } + /// Unlock a user. This is only available to administrators. + async fn unlock_user( + &self, + ctx: &Context<'_>, + input: UnlockUserInput, + ) -> Result { + let state = ctx.state(); + let requester = ctx.requester(); + let matrix = state.homeserver_connection(); + + if !requester.is_admin() { + return Err(async_graphql::Error::new("Unauthorized")); + } + + let mut repo = state.repository().await?; + let user_id = NodeType::User.extract_ulid(&input.user_id)?; + let user = repo.user().lookup(user_id).await?; + + let Some(user) = user else { + return Ok(UnlockUserPayload::NotFound); + }; + + // Call the homeserver synchronously to unlock the user + let mxid = matrix.mxid(&user.username); + matrix.reactivate_user(&mxid).await?; + + // Now unlock the user in our database + let user = repo.user().unlock(user).await?; + + repo.save().await?; + + Ok(UnlockUserPayload::Unlocked(user)) + } + /// Set whether a user can request admin. This is only available to /// administrators. async fn set_can_request_admin( diff --git a/frontend/schema.graphql b/frontend/schema.graphql index a5eab5b3e..d7aaf6749 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -735,6 +735,10 @@ type Mutation { """ lockUser(input: LockUserInput!): LockUserPayload! """ + Unlock a user. This is only available to administrators. + """ + unlockUser(input: UnlockUserInput!): UnlockUserPayload! + """ Set whether a user can request admin. This is only available to administrators. """ @@ -1399,6 +1403,44 @@ type SiteConfig implements Node { id: ID! } +""" +The input for the `unlockUser` mutation. +""" +input UnlockUserInput { + """ + The ID of the user to unlock + """ + userId: ID! +} + +""" +The payload for the `unlockUser` mutation. +""" +type UnlockUserPayload { + """ + Status of the operation + """ + status: UnlockUserStatus! + """ + The user that was unlocked. + """ + user: User +} + +""" +The status of the `unlockUser` mutation. +""" +enum UnlockUserStatus { + """ + The user was unlocked. + """ + UNLOCKED + """ + The user was not found. + """ + NOT_FOUND +} + type UpstreamOAuth2Link implements Node & CreationEvent { """ ID of the object. diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index dea242625..35c3adb58 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -497,6 +497,8 @@ export type Mutation = { setPassword: SetPasswordPayload; /** Set an email address as primary */ setPrimaryEmail: SetPrimaryEmailPayload; + /** Unlock a user. This is only available to administrators. */ + unlockUser: UnlockUserPayload; /** Submit a verification code for an email address */ verifyEmail: VerifyEmailPayload; }; @@ -586,6 +588,12 @@ export type MutationSetPrimaryEmailArgs = { }; +/** The mutations root of the GraphQL interface. */ +export type MutationUnlockUserArgs = { + input: UnlockUserInput; +}; + + /** The mutations root of the GraphQL interface. */ export type MutationVerifyEmailArgs = { input: VerifyEmailInput; @@ -1040,6 +1048,29 @@ export type SiteConfig = Node & { tosUri?: Maybe; }; +/** The input for the `unlockUser` mutation. */ +export type UnlockUserInput = { + /** The ID of the user to unlock */ + userId: Scalars['ID']['input']; +}; + +/** The payload for the `unlockUser` mutation. */ +export type UnlockUserPayload = { + __typename?: 'UnlockUserPayload'; + /** Status of the operation */ + status: UnlockUserStatus; + /** The user that was unlocked. */ + user?: Maybe; +}; + +/** The status of the `unlockUser` mutation. */ +export enum UnlockUserStatus { + /** The user was not found. */ + NotFound = 'NOT_FOUND', + /** The user was unlocked. */ + Unlocked = 'UNLOCKED' +} + export type UpstreamOAuth2Link = CreationEvent & Node & { __typename?: 'UpstreamOAuth2Link'; /** When the object was created. */ diff --git a/frontend/src/gql/schema.ts b/frontend/src/gql/schema.ts index 20fd4d780..298645ee8 100644 --- a/frontend/src/gql/schema.ts +++ b/frontend/src/gql/schema.ts @@ -1468,6 +1468,29 @@ export default { } ] }, + { + "name": "unlockUser", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "OBJECT", + "name": "UnlockUserPayload", + "ofType": null + } + }, + "args": [ + { + "name": "input", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Any" + } + } + } + ] + }, { "name": "verifyEmail", "type": { @@ -2622,6 +2645,33 @@ export default { } ] }, + { + "kind": "OBJECT", + "name": "UnlockUserPayload", + "fields": [ + { + "name": "status", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Any" + } + }, + "args": [] + }, + { + "name": "user", + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "args": [] + } + ], + "interfaces": [] + }, { "kind": "OBJECT", "name": "UpstreamOAuth2Link",