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}
82
83impl<'a> UserFilter<'a> {
84    /// Create a new [`UserFilter`] with default values
85    #[must_use]
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    /// Filter for active users
91    #[must_use]
92    pub fn active_only(mut self) -> Self {
93        self.state = Some(UserState::Active);
94        self
95    }
96
97    /// Filter for locked users
98    #[must_use]
99    pub fn locked_only(mut self) -> Self {
100        self.state = Some(UserState::Locked);
101        self
102    }
103
104    /// Filter for deactivated users
105    #[must_use]
106    pub fn deactivated_only(mut self) -> Self {
107        self.state = Some(UserState::Deactivated);
108        self
109    }
110
111    /// Filter for users that can request admin privileges
112    #[must_use]
113    pub fn can_request_admin_only(mut self) -> Self {
114        self.can_request_admin = Some(true);
115        self
116    }
117
118    /// Filter for users that can't request admin privileges
119    #[must_use]
120    pub fn cannot_request_admin_only(mut self) -> Self {
121        self.can_request_admin = Some(false);
122        self
123    }
124
125    /// Filter for guest users
126    #[must_use]
127    pub fn guest_only(mut self) -> Self {
128        self.is_guest = Some(true);
129        self
130    }
131
132    /// Filter for non-guest users
133    #[must_use]
134    pub fn non_guest_only(mut self) -> Self {
135        self.is_guest = Some(false);
136        self
137    }
138
139    /// Filter for users that match the given search string
140    #[must_use]
141    pub fn matching_search(mut self, search: &'a str) -> Self {
142        self.search = Some(search);
143        self
144    }
145
146    /// Get the state filter
147    ///
148    /// Returns [`None`] if no state filter was set
149    #[must_use]
150    pub fn state(&self) -> Option<UserState> {
151        self.state
152    }
153
154    /// Get the can request admin filter
155    ///
156    /// Returns [`None`] if no can request admin filter was set
157    #[must_use]
158    pub fn can_request_admin(&self) -> Option<bool> {
159        self.can_request_admin
160    }
161
162    /// Get the is guest filter
163    ///
164    /// Returns [`None`] if no is guest filter was set
165    #[must_use]
166    pub fn is_guest(&self) -> Option<bool> {
167        self.is_guest
168    }
169
170    /// Get the search filter
171    ///
172    /// Returns [`None`] if no search filter was set
173    #[must_use]
174    pub fn search(&self) -> Option<&'a str> {
175        self.search
176    }
177}
178
179/// A [`UserRepository`] helps interacting with [`User`] saved in the storage
180/// backend
181#[async_trait]
182pub trait UserRepository: Send + Sync {
183    /// The error type returned by the repository
184    type Error;
185
186    /// Lookup a [`User`] by its ID
187    ///
188    /// Returns `None` if no [`User`] was found
189    ///
190    /// # Parameters
191    ///
192    /// * `id`: The ID of the [`User`] to lookup
193    ///
194    /// # Errors
195    ///
196    /// Returns [`Self::Error`] if the underlying repository fails
197    async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
198
199    /// Find a [`User`] by its username, in a case-insensitive manner
200    ///
201    /// Returns `None` if no [`User`] was found
202    ///
203    /// # Parameters
204    ///
205    /// * `username`: The username of the [`User`] to lookup
206    ///
207    /// # Errors
208    ///
209    /// Returns [`Self::Error`] if the underlying repository fails
210    async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>;
211
212    /// Create a new [`User`]
213    ///
214    /// Returns the newly created [`User`]
215    ///
216    /// # Parameters
217    ///
218    /// * `rng`: A random number generator to generate the [`User`] ID
219    /// * `clock`: The clock used to generate timestamps
220    /// * `username`: The username of the [`User`]
221    ///
222    /// # Errors
223    ///
224    /// Returns [`Self::Error`] if the underlying repository fails
225    async fn add(
226        &mut self,
227        rng: &mut (dyn RngCore + Send),
228        clock: &dyn Clock,
229        username: String,
230    ) -> Result<User, Self::Error>;
231
232    /// Check if a [`User`] exists
233    ///
234    /// Returns `true` if the [`User`] exists, `false` otherwise
235    ///
236    /// # Parameters
237    ///
238    /// * `username`: The username of the [`User`] to lookup
239    ///
240    /// # Errors
241    ///
242    /// Returns [`Self::Error`] if the underlying repository fails
243    async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
244
245    /// Lock a [`User`]
246    ///
247    /// Returns the locked [`User`]
248    ///
249    /// # Parameters
250    ///
251    /// * `clock`: The clock used to generate timestamps
252    /// * `user`: The [`User`] to lock
253    ///
254    /// # Errors
255    ///
256    /// Returns [`Self::Error`] if the underlying repository fails
257    async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
258
259    /// Unlock a [`User`]
260    ///
261    /// Returns the unlocked [`User`]
262    ///
263    /// # Parameters
264    ///
265    /// * `user`: The [`User`] to unlock
266    ///
267    /// # Errors
268    ///
269    /// Returns [`Self::Error`] if the underlying repository fails
270    async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
271
272    /// Deactivate a [`User`]
273    ///
274    /// Returns the deactivated [`User`]
275    ///
276    /// # Parameters
277    ///
278    /// * `clock`: The clock used to generate timestamps
279    /// * `user`: The [`User`] to deactivate
280    ///
281    /// # Errors
282    ///
283    /// Returns [`Self::Error`] if the underlying repository fails
284    async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
285
286    /// Reactivate a [`User`]
287    ///
288    /// Returns the reactivated [`User`]
289    ///
290    /// # Parameters
291    ///
292    /// * `user`: The [`User`] to reactivate
293    ///
294    /// # Errors
295    ///
296    /// Returns [`Self::Error`] if the underlying repository fails
297    async fn reactivate(&mut self, user: User) -> Result<User, Self::Error>;
298
299    /// Delete all the unsupported third-party IDs of a [`User`].
300    ///
301    /// Those were imported by syn2mas and kept in case we wanted to support
302    /// them later. They still need to be cleaned up when a user deactivate
303    /// their account.
304    ///
305    /// Returns the number of deleted third-party IDs.
306    ///
307    /// # Parameters
308    ///
309    /// * `user`: The [`User`] whose unsupported third-party IDs should be
310    ///   deleted
311    ///
312    /// # Errors
313    ///
314    /// Returns [`Self::Error`] if the underlying repository fails
315    async fn delete_unsupported_threepids(&mut self, user: &User) -> Result<usize, Self::Error>;
316
317    /// Set whether a [`User`] can request admin
318    ///
319    /// Returns the [`User`] with the new `can_request_admin` value
320    ///
321    /// # Parameters
322    ///
323    /// * `user`: The [`User`] to update
324    ///
325    /// # Errors
326    ///
327    /// Returns [`Self::Error`] if the underlying repository fails
328    async fn set_can_request_admin(
329        &mut self,
330        user: User,
331        can_request_admin: bool,
332    ) -> Result<User, Self::Error>;
333
334    /// List [`User`] with the given filter and pagination
335    ///
336    /// # Parameters
337    ///
338    /// * `filter`: The filter parameters
339    /// * `pagination`: The pagination parameters
340    ///
341    /// # Errors
342    ///
343    /// Returns [`Self::Error`] if the underlying repository fails
344    async fn list(
345        &mut self,
346        filter: UserFilter<'_>,
347        pagination: Pagination,
348    ) -> Result<Page<User>, Self::Error>;
349
350    /// Count the [`User`] with the given filter
351    ///
352    /// # Parameters
353    ///
354    /// * `filter`: The filter parameters
355    ///
356    /// # Errors
357    ///
358    /// Returns [`Self::Error`] if the underlying repository fails
359    async fn count(&mut self, filter: UserFilter<'_>) -> Result<usize, Self::Error>;
360
361    /// Acquire a lock on the user to make sure device operations are done in a
362    /// sequential way. The lock is released when the repository is saved or
363    /// rolled back.
364    ///
365    /// # Parameters
366    ///
367    /// * `user`: The user to lock
368    ///
369    /// # Errors
370    ///
371    /// Returns [`Self::Error`] if the underlying repository fails
372    async fn acquire_lock_for_sync(&mut self, user: &User) -> Result<(), Self::Error>;
373}
374
375repository_impl!(UserRepository:
376    async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
377    async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>;
378    async fn add(
379        &mut self,
380        rng: &mut (dyn RngCore + Send),
381        clock: &dyn Clock,
382        username: String,
383    ) -> Result<User, Self::Error>;
384    async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
385    async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
386    async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
387    async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
388    async fn reactivate(&mut self, user: User) -> Result<User, Self::Error>;
389    async fn delete_unsupported_threepids(&mut self, user: &User) -> Result<usize, Self::Error>;
390    async fn set_can_request_admin(
391        &mut self,
392        user: User,
393        can_request_admin: bool,
394    ) -> Result<User, Self::Error>;
395    async fn list(
396        &mut self,
397        filter: UserFilter<'_>,
398        pagination: Pagination,
399    ) -> Result<Page<User>, Self::Error>;
400    async fn count(&mut self, filter: UserFilter<'_>) -> Result<usize, Self::Error>;
401    async fn acquire_lock_for_sync(&mut self, user: &User) -> Result<(), Self::Error>;
402);