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