mas_handlers/graphql/query/
user.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use async_graphql::{
8    Context, Enum, ID, Object,
9    connection::{Connection, Edge, OpaqueCursor, query},
10};
11use mas_storage::{Pagination, user::UserFilter};
12
13use crate::graphql::{
14    UserId,
15    model::{Cursor, NodeCursor, NodeType, PreloadedTotalCount, User},
16    state::ContextExt as _,
17};
18
19#[derive(Default)]
20pub struct UserQuery;
21
22#[Object]
23impl UserQuery {
24    /// Fetch a user by its ID.
25    pub async fn user(
26        &self,
27        ctx: &Context<'_>,
28        id: ID,
29    ) -> Result<Option<User>, async_graphql::Error> {
30        let id = NodeType::User.extract_ulid(&id)?;
31
32        let requester = ctx.requester();
33        if !requester.is_owner_or_admin(&UserId(id)) {
34            return Ok(None);
35        }
36
37        // We could avoid the database lookup if the requester is the user we're looking
38        // for but that would make the code more complex and we're not very
39        // concerned about performance yet
40        let state = ctx.state();
41        let mut repo = state.repository().await?;
42        let user = repo.user().lookup(id).await?;
43        repo.cancel().await?;
44
45        Ok(user.map(User))
46    }
47
48    /// Fetch a user by its username.
49    async fn user_by_username(
50        &self,
51        ctx: &Context<'_>,
52        username: String,
53    ) -> Result<Option<User>, async_graphql::Error> {
54        let requester = ctx.requester();
55        let state = ctx.state();
56        let mut repo = state.repository().await?;
57
58        let user = repo.user().find_by_username(&username).await?;
59        let Some(user) = user else {
60            // We don't want to leak the existence of a user
61            return Ok(None);
62        };
63
64        // Users can only see themselves, except for admins
65        if !requester.is_owner_or_admin(&user) {
66            return Ok(None);
67        }
68
69        Ok(Some(User(user)))
70    }
71
72    /// Get a list of users.
73    ///
74    /// This is only available to administrators.
75    async fn users(
76        &self,
77        ctx: &Context<'_>,
78
79        #[graphql(name = "state", desc = "List only users with the given state.")]
80        state_param: Option<UserState>,
81
82        #[graphql(
83            name = "canRequestAdmin",
84            desc = "List only users with the given 'canRequestAdmin' value"
85        )]
86        can_request_admin_param: Option<bool>,
87
88        #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
89        after: Option<String>,
90        #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
91        before: Option<String>,
92        #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
93        #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
94    ) -> Result<Connection<Cursor, User, PreloadedTotalCount>, async_graphql::Error> {
95        let requester = ctx.requester();
96        if !requester.is_admin() {
97            return Err(async_graphql::Error::new("Unauthorized"));
98        }
99
100        let state = ctx.state();
101        let mut repo = state.repository().await?;
102
103        query(
104            after,
105            before,
106            first,
107            last,
108            async |after, before, first, last| {
109                let after_id = after
110                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::User))
111                    .transpose()?;
112                let before_id = before
113                    .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::User))
114                    .transpose()?;
115                let pagination = Pagination::try_new(before_id, after_id, first, last)?;
116
117                // Build the query filter
118                let filter = UserFilter::new();
119                let filter = match can_request_admin_param {
120                    Some(true) => filter.can_request_admin_only(),
121                    Some(false) => filter.cannot_request_admin_only(),
122                    None => filter,
123                };
124                let filter = match state_param {
125                    Some(UserState::Active) => filter.active_only(),
126                    Some(UserState::Locked) => filter.locked_only(),
127                    None => filter,
128                };
129
130                let page = repo.user().list(filter, pagination).await?;
131
132                // Preload the total count if requested
133                let count = if ctx.look_ahead().field("totalCount").exists() {
134                    Some(repo.user().count(filter).await?)
135                } else {
136                    None
137                };
138
139                repo.cancel().await?;
140
141                let mut connection = Connection::with_additional_fields(
142                    page.has_previous_page,
143                    page.has_next_page,
144                    PreloadedTotalCount(count),
145                );
146                connection.edges.extend(
147                    page.edges.into_iter().map(|p| {
148                        Edge::new(OpaqueCursor(NodeCursor(NodeType::User, p.id)), User(p))
149                    }),
150                );
151
152                Ok::<_, async_graphql::Error>(connection)
153            },
154        )
155        .await
156    }
157}
158
159/// The state of a user.
160#[derive(Enum, Copy, Clone, Eq, PartialEq)]
161enum UserState {
162    /// The user is active.
163    Active,
164
165    /// The user is locked.
166    Locked,
167}