Skip to main content

mas_storage/user/
mod.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8//! Repositories to interact with entities related to user accounts
9
10use async_trait::async_trait;
11use mas_data_model::{Clock, User};
12use rand_core::RngCore;
13use ulid::Ulid;
14
15use crate::{Page, Pagination, repository_impl};
16
17mod email;
18mod password;
19mod recovery;
20mod registration;
21mod registration_token;
22mod session;
23mod terms;
24
25pub use self::{
26    email::{UserEmailFilter, UserEmailRepository},
27    password::UserPasswordRepository,
28    recovery::UserRecoveryRepository,
29    registration::UserRegistrationRepository,
30    registration_token::{UserRegistrationTokenFilter, UserRegistrationTokenRepository},
31    session::{BrowserSessionFilter, BrowserSessionRepository},
32    terms::UserTermsRepository,
33};
34
35/// The state of a user account
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum UserState {
38    /// The account is deactivated, it has the `deactivated_at` timestamp set
39    Deactivated,
40
41    /// The account is locked, it has the `locked_at` timestamp set
42    Locked,
43
44    /// The account is active
45    Active,
46}
47
48impl UserState {
49    /// Returns `true` if the user state is [`Locked`].
50    ///
51    /// [`Locked`]: UserState::Locked
52    #[must_use]
53    pub fn is_locked(&self) -> bool {
54        matches!(self, Self::Locked)
55    }
56
57    /// Returns `true` if the user state is [`Deactivated`].
58    ///
59    /// [`Deactivated`]: UserState::Deactivated
60    #[must_use]
61    pub fn is_deactivated(&self) -> bool {
62        matches!(self, Self::Deactivated)
63    }
64
65    /// Returns `true` if the user state is [`Active`].
66    ///
67    /// [`Active`]: UserState::Active
68    #[must_use]
69    pub fn is_active(&self) -> bool {
70        matches!(self, Self::Active)
71    }
72}
73
74/// Filter parameters for listing users
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
76pub struct UserFilter<'a> {
77    state: Option<UserState>,
78    can_request_admin: Option<bool>,
79    is_guest: Option<bool>,
80    search: Option<&'a str>,
81    active_oauth2_session_for_any_of_clients: Option<&'a [Ulid]>,
82    has_active_oauth2_session: Option<bool>,
83    has_active_compat_session: Option<bool>,
84}
85
86impl<'a> UserFilter<'a> {
87    /// Create a new [`UserFilter`] with default values
88    #[must_use]
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// Filter for active users
94    #[must_use]
95    pub fn active_only(mut self) -> Self {
96        self.state = Some(UserState::Active);
97        self
98    }
99
100    /// Filter for locked users
101    #[must_use]
102    pub fn locked_only(mut self) -> Self {
103        self.state = Some(UserState::Locked);
104        self
105    }
106
107    /// Filter for deactivated users
108    #[must_use]
109    pub fn deactivated_only(mut self) -> Self {
110        self.state = Some(UserState::Deactivated);
111        self
112    }
113
114    /// Filter for users that can request admin privileges
115    #[must_use]
116    pub fn can_request_admin_only(mut self) -> Self {
117        self.can_request_admin = Some(true);
118        self
119    }
120
121    /// Filter for users that can't request admin privileges
122    #[must_use]
123    pub fn cannot_request_admin_only(mut self) -> Self {
124        self.can_request_admin = Some(false);
125        self
126    }
127
128    /// Filter for guest users
129    #[must_use]
130    pub fn guest_only(mut self) -> Self {
131        self.is_guest = Some(true);
132        self
133    }
134
135    /// Filter for non-guest users
136    #[must_use]
137    pub fn non_guest_only(mut self) -> Self {
138        self.is_guest = Some(false);
139        self
140    }
141
142    /// Filter for users that match the given search string
143    #[must_use]
144    pub fn matching_search(mut self, search: &'a str) -> Self {
145        self.search = Some(search);
146        self
147    }
148
149    /// Filter for users which have at least one active (non-finished)
150    /// `OAuth2` session belonging to any of the given clients.
151    ///
152    /// The semantics are OR across the clients: a user matches if they have
153    /// an active session for *any* of the supplied client IDs.
154    #[must_use]
155    pub fn with_active_oauth2_session_for_any_of_clients(mut self, clients: &'a [Ulid]) -> Self {
156        self.active_oauth2_session_for_any_of_clients = Some(clients);
157        self
158    }
159
160    /// Filter for users which have (or don't have) at least one active
161    /// (non-finished) `OAuth2` session, regardless of the client.
162    #[must_use]
163    pub fn with_active_oauth2_session(mut self, has: bool) -> Self {
164        self.has_active_oauth2_session = Some(has);
165        self
166    }
167
168    /// Filter for users which have (or don't have) at least one active
169    /// (non-finished) compatibility session.
170    #[must_use]
171    pub fn with_active_compat_session(mut self, has: bool) -> Self {
172        self.has_active_compat_session = Some(has);
173        self
174    }
175
176    /// Get the state filter
177    ///
178    /// Returns [`None`] if no state filter was set
179    #[must_use]
180    pub fn state(&self) -> Option<UserState> {
181        self.state
182    }
183
184    /// Get the can request admin filter
185    ///
186    /// Returns [`None`] if no can request admin filter was set
187    #[must_use]
188    pub fn can_request_admin(&self) -> Option<bool> {
189        self.can_request_admin
190    }
191
192    /// Get the is guest filter
193    ///
194    /// Returns [`None`] if no is guest filter was set
195    #[must_use]
196    pub fn is_guest(&self) -> Option<bool> {
197        self.is_guest
198    }
199
200    /// Get the search filter
201    ///
202    /// Returns [`None`] if no search filter was set
203    #[must_use]
204    pub fn search(&self) -> Option<&'a str> {
205        self.search
206    }
207
208    /// Get the active-`OAuth2`-session-for-any-of-clients filter
209    ///
210    /// Returns [`None`] if no such filter was set
211    #[must_use]
212    pub fn active_oauth2_session_for_any_of_clients(&self) -> Option<&'a [Ulid]> {
213        self.active_oauth2_session_for_any_of_clients
214    }
215
216    /// Get the has-active-`OAuth2`-session filter
217    ///
218    /// Returns [`None`] if no such filter was set
219    #[must_use]
220    pub fn has_active_oauth2_session(&self) -> Option<bool> {
221        self.has_active_oauth2_session
222    }
223
224    /// Get the has-active-compat-session filter
225    ///
226    /// Returns [`None`] if no such filter was set
227    #[must_use]
228    pub fn has_active_compat_session(&self) -> Option<bool> {
229        self.has_active_compat_session
230    }
231}
232
233/// A [`UserRepository`] helps interacting with [`User`] saved in the storage
234/// backend
235#[async_trait]
236pub trait UserRepository: Send + Sync {
237    /// The error type returned by the repository
238    type Error;
239
240    /// Lookup a [`User`] by its ID
241    ///
242    /// Returns `None` if no [`User`] was found
243    ///
244    /// # Parameters
245    ///
246    /// * `id`: The ID of the [`User`] to lookup
247    ///
248    /// # Errors
249    ///
250    /// Returns [`Self::Error`] if the underlying repository fails
251    async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
252
253    /// Find a [`User`] by its username, in a case-insensitive manner
254    ///
255    /// Returns `None` if no [`User`] was found
256    ///
257    /// # Parameters
258    ///
259    /// * `username`: The username of the [`User`] to lookup
260    ///
261    /// # Errors
262    ///
263    /// Returns [`Self::Error`] if the underlying repository fails
264    async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>;
265
266    /// Create a new [`User`]
267    ///
268    /// Returns the newly created [`User`]
269    ///
270    /// # Parameters
271    ///
272    /// * `rng`: A random number generator to generate the [`User`] ID
273    /// * `clock`: The clock used to generate timestamps
274    /// * `username`: The username of the [`User`]
275    ///
276    /// # Errors
277    ///
278    /// Returns [`Self::Error`] if the underlying repository fails
279    async fn add(
280        &mut self,
281        rng: &mut (dyn RngCore + Send),
282        clock: &dyn Clock,
283        username: String,
284    ) -> Result<User, Self::Error>;
285
286    /// Check if a [`User`] exists
287    ///
288    /// Returns `true` if the [`User`] exists, `false` otherwise
289    ///
290    /// # Parameters
291    ///
292    /// * `username`: The username of the [`User`] to lookup
293    ///
294    /// # Errors
295    ///
296    /// Returns [`Self::Error`] if the underlying repository fails
297    async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
298
299    /// Lock a [`User`]
300    ///
301    /// Returns the locked [`User`]
302    ///
303    /// # Parameters
304    ///
305    /// * `clock`: The clock used to generate timestamps
306    /// * `user`: The [`User`] to lock
307    ///
308    /// # Errors
309    ///
310    /// Returns [`Self::Error`] if the underlying repository fails
311    async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
312
313    /// Unlock a [`User`]
314    ///
315    /// Returns the unlocked [`User`]
316    ///
317    /// # Parameters
318    ///
319    /// * `user`: The [`User`] to unlock
320    ///
321    /// # Errors
322    ///
323    /// Returns [`Self::Error`] if the underlying repository fails
324    async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
325
326    /// Deactivate a [`User`]
327    ///
328    /// Returns the deactivated [`User`]
329    ///
330    /// # Parameters
331    ///
332    /// * `clock`: The clock used to generate timestamps
333    /// * `user`: The [`User`] to deactivate
334    ///
335    /// # Errors
336    ///
337    /// Returns [`Self::Error`] if the underlying repository fails
338    async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
339
340    /// Reactivate a [`User`]
341    ///
342    /// Returns the reactivated [`User`]
343    ///
344    /// # Parameters
345    ///
346    /// * `user`: The [`User`] to reactivate
347    ///
348    /// # Errors
349    ///
350    /// Returns [`Self::Error`] if the underlying repository fails
351    async fn reactivate(&mut self, user: User) -> Result<User, Self::Error>;
352
353    /// Delete all the unsupported third-party IDs of a [`User`].
354    ///
355    /// Those were imported by syn2mas and kept in case we wanted to support
356    /// them later. They still need to be cleaned up when a user deactivates
357    /// their account.
358    ///
359    /// Returns the number of deleted third-party IDs.
360    ///
361    /// # Parameters
362    ///
363    /// * `user`: The [`User`] whose unsupported third-party IDs should be
364    ///   deleted
365    ///
366    /// # Errors
367    ///
368    /// Returns [`Self::Error`] if the underlying repository fails
369    async fn delete_unsupported_threepids(&mut self, user: &User) -> Result<usize, Self::Error>;
370
371    /// Set whether a [`User`] can request admin
372    ///
373    /// Returns the [`User`] with the new `can_request_admin` value
374    ///
375    /// # Parameters
376    ///
377    /// * `user`: The [`User`] to update
378    ///
379    /// # Errors
380    ///
381    /// Returns [`Self::Error`] if the underlying repository fails
382    async fn set_can_request_admin(
383        &mut self,
384        user: User,
385        can_request_admin: bool,
386    ) -> Result<User, Self::Error>;
387
388    /// List [`User`] with the given filter and pagination
389    ///
390    /// # Parameters
391    ///
392    /// * `filter`: The filter parameters
393    /// * `pagination`: The pagination parameters
394    ///
395    /// # Errors
396    ///
397    /// Returns [`Self::Error`] if the underlying repository fails
398    async fn list(
399        &mut self,
400        filter: UserFilter<'_>,
401        pagination: Pagination,
402    ) -> Result<Page<User>, Self::Error>;
403
404    /// Count the [`User`] with the given filter
405    ///
406    /// # Parameters
407    ///
408    /// * `filter`: The filter parameters
409    ///
410    /// # Errors
411    ///
412    /// Returns [`Self::Error`] if the underlying repository fails
413    async fn count(&mut self, filter: UserFilter<'_>) -> Result<usize, Self::Error>;
414
415    /// Acquire a lock on the user to make sure device operations are done in a
416    /// sequential way. The lock is released when the repository is saved or
417    /// rolled back.
418    ///
419    /// # Parameters
420    ///
421    /// * `user`: The user to lock
422    ///
423    /// # Errors
424    ///
425    /// Returns [`Self::Error`] if the underlying repository fails
426    async fn acquire_lock_for_sync(&mut self, user: &User) -> Result<(), Self::Error>;
427}
428
429repository_impl!(UserRepository:
430    async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
431    async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>;
432    async fn add(
433        &mut self,
434        rng: &mut (dyn RngCore + Send),
435        clock: &dyn Clock,
436        username: String,
437    ) -> Result<User, Self::Error>;
438    async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
439    async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
440    async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
441    async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
442    async fn reactivate(&mut self, user: User) -> Result<User, Self::Error>;
443    async fn delete_unsupported_threepids(&mut self, user: &User) -> Result<usize, Self::Error>;
444    async fn set_can_request_admin(
445        &mut self,
446        user: User,
447        can_request_admin: bool,
448    ) -> Result<User, Self::Error>;
449    async fn list(
450        &mut self,
451        filter: UserFilter<'_>,
452        pagination: Pagination,
453    ) -> Result<Page<User>, Self::Error>;
454    async fn count(&mut self, filter: UserFilter<'_>) -> Result<usize, Self::Error>;
455    async fn acquire_lock_for_sync(&mut self, user: &User) -> Result<(), Self::Error>;
456);