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.
67use async_graphql::{
8 Context, Enum, ID, Object,
9 connection::{Connection, Edge, OpaqueCursor, query},
10};
11use mas_storage::{Pagination, user::UserFilter};
1213use crate::graphql::{
14 UserId,
15 model::{Cursor, NodeCursor, NodeType, PreloadedTotalCount, User},
16 state::ContextExt as _,
17};
1819#[derive(Default)]
20pub struct UserQuery;
2122#[Object]
23impl UserQuery {
24/// Fetch a user by its ID.
25pub async fn user(
26&self,
27 ctx: &Context<'_>,
28 id: ID,
29 ) -> Result<Option<User>, async_graphql::Error> {
30let id = NodeType::User.extract_ulid(&id)?;
3132let requester = ctx.requester();
33if !requester.is_owner_or_admin(&UserId(id)) {
34return Ok(None);
35 }
3637// 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
40let state = ctx.state();
41let mut repo = state.repository().await?;
42let user = repo.user().lookup(id).await?;
43 repo.cancel().await?;
4445Ok(user.map(User))
46 }
4748/// Fetch a user by its username.
49async fn user_by_username(
50&self,
51 ctx: &Context<'_>,
52 username: String,
53 ) -> Result<Option<User>, async_graphql::Error> {
54let requester = ctx.requester();
55let state = ctx.state();
56let mut repo = state.repository().await?;
5758let user = repo.user().find_by_username(&username).await?;
59let Some(user) = user else {
60// We don't want to leak the existence of a user
61return Ok(None);
62 };
6364// Users can only see themselves, except for admins
65if !requester.is_owner_or_admin(&user) {
66return Ok(None);
67 }
6869Ok(Some(User(user)))
70 }
7172/// Get a list of users.
73 ///
74 /// This is only available to administrators.
75async fn users(
76&self,
77 ctx: &Context<'_>,
7879#[graphql(name = "state", desc = "List only users with the given state.")]
80state_param: Option<UserState>,
8182#[graphql(
83 name = "canRequestAdmin",
84 desc = "List only users with the given 'canRequestAdmin' value"
85)]
86can_request_admin_param: Option<bool>,
8788#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
89after: Option<String>,
90#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
91before: 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> {
95let requester = ctx.requester();
96if !requester.is_admin() {
97return Err(async_graphql::Error::new("Unauthorized"));
98 }
99100let state = ctx.state();
101let mut repo = state.repository().await?;
102103 query(
104 after,
105 before,
106 first,
107 last,
108async |after, before, first, last| {
109let after_id = after
110 .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::User))
111 .transpose()?;
112let before_id = before
113 .map(|x: OpaqueCursor<NodeCursor>| x.extract_for_type(NodeType::User))
114 .transpose()?;
115let pagination = Pagination::try_new(before_id, after_id, first, last)?;
116117// Build the query filter
118let filter = UserFilter::new();
119let filter = match can_request_admin_param {
120Some(true) => filter.can_request_admin_only(),
121Some(false) => filter.cannot_request_admin_only(),
122None => filter,
123 };
124let filter = match state_param {
125Some(UserState::Active) => filter.active_only(),
126Some(UserState::Locked) => filter.locked_only(),
127None => filter,
128 };
129130let page = repo.user().list(filter, pagination).await?;
131132// Preload the total count if requested
133let count = if ctx.look_ahead().field("totalCount").exists() {
134Some(repo.user().count(filter).await?)
135 } else {
136None
137};
138139 repo.cancel().await?;
140141let 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 );
151152Ok::<_, async_graphql::Error>(connection)
153 },
154 )
155 .await
156}
157}
158159/// The state of a user.
160#[derive(Enum, Copy, Clone, Eq, PartialEq)]
161enum UserState {
162/// The user is active.
163Active,
164165/// The user is locked.
166Locked,
167}