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);