Skip to main content

mas_data_model/
users.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
8use std::net::IpAddr;
9
10use chrono::{DateTime, Utc};
11use rand::RngExt;
12use serde::Serialize;
13use ulid::Ulid;
14use url::Url;
15
16use crate::UlidExt as _;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub struct MatrixUser {
20    pub mxid: String,
21    pub display_name: Option<String>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25pub struct User {
26    pub id: Ulid,
27    pub username: String,
28    pub sub: String,
29    pub created_at: DateTime<Utc>,
30    pub locked_at: Option<DateTime<Utc>>,
31    pub deactivated_at: Option<DateTime<Utc>>,
32    pub can_request_admin: bool,
33    pub is_guest: bool,
34}
35
36impl User {
37    /// Returns `true` unless the user is locked or deactivated.
38    #[must_use]
39    pub fn is_valid(&self) -> bool {
40        self.locked_at.is_none() && self.deactivated_at.is_none()
41    }
42
43    /// Returns `true` if the user is a valid actor, for example
44    /// of a personal session.
45    ///
46    /// Currently: this is `true` unless the user is deactivated.
47    ///
48    /// This is a weaker form of validity: `is_valid` always implies
49    /// `is_valid_actor`, but some users (currently: locked users)
50    /// can be valid actors for personal sessions but aren't valid
51    /// except through administrative access.
52    #[must_use]
53    pub fn is_valid_actor(&self) -> bool {
54        self.deactivated_at.is_none()
55    }
56}
57
58impl User {
59    #[doc(hidden)]
60    #[must_use]
61    pub fn samples(now: chrono::DateTime<Utc>, rng: &mut impl RngExt) -> Vec<Self> {
62        vec![User {
63            id: Ulid::from_datetime_with_rng(now, rng),
64            username: "john".to_owned(),
65            sub: "123-456".to_owned(),
66            created_at: now,
67            locked_at: None,
68            deactivated_at: None,
69            can_request_admin: false,
70            is_guest: false,
71        }]
72    }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
76pub struct Password {
77    pub id: Ulid,
78    pub hashed_password: String,
79    pub version: u16,
80    pub upgraded_from_id: Option<Ulid>,
81    pub created_at: DateTime<Utc>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
85pub struct Authentication {
86    pub id: Ulid,
87    pub created_at: DateTime<Utc>,
88    pub authentication_method: AuthenticationMethod,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92pub enum AuthenticationMethod {
93    Password { user_password_id: Ulid },
94    UpstreamOAuth2 { upstream_oauth2_session_id: Ulid },
95    Unknown,
96}
97
98/// A session to recover a user if they have lost their credentials
99///
100/// For each session intiated, there may be multiple [`UserRecoveryTicket`]s
101/// sent to the user, either because multiple [`User`] have the same email
102/// address, or because the user asked to send the recovery email again.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
104pub struct UserRecoverySession {
105    pub id: Ulid,
106    pub email: String,
107    pub user_agent: String,
108    pub ip_address: Option<IpAddr>,
109    pub locale: String,
110    pub created_at: DateTime<Utc>,
111    pub consumed_at: Option<DateTime<Utc>>,
112}
113
114/// A single recovery ticket for a user recovery session
115///
116/// Whenever a new recovery session is initiated, a new ticket is created for
117/// each email address matching in the database. That ticket is sent by email,
118/// as a link that the user can click to recover their account.
119#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
120pub struct UserRecoveryTicket {
121    pub id: Ulid,
122    pub user_recovery_session_id: Ulid,
123    pub user_email_id: Ulid,
124    pub ticket: String,
125    pub created_at: DateTime<Utc>,
126    pub expires_at: DateTime<Utc>,
127}
128
129impl UserRecoveryTicket {
130    #[must_use]
131    pub fn active(&self, now: DateTime<Utc>) -> bool {
132        now < self.expires_at
133    }
134}
135
136/// A user email authentication session
137#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
138pub struct UserEmailAuthentication {
139    pub id: Ulid,
140    pub user_session_id: Option<Ulid>,
141    pub user_registration_id: Option<Ulid>,
142    pub email: String,
143    pub created_at: DateTime<Utc>,
144    pub completed_at: Option<DateTime<Utc>>,
145}
146
147/// A user email authentication code
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
149pub struct UserEmailAuthenticationCode {
150    pub id: Ulid,
151    pub user_email_authentication_id: Ulid,
152    pub code: String,
153    pub created_at: DateTime<Utc>,
154    pub expires_at: DateTime<Utc>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
158pub struct BrowserSession {
159    pub id: Ulid,
160    pub user: User,
161    pub created_at: DateTime<Utc>,
162    pub finished_at: Option<DateTime<Utc>>,
163    pub user_agent: Option<String>,
164    pub last_active_at: Option<DateTime<Utc>>,
165    pub last_active_ip: Option<IpAddr>,
166}
167
168impl BrowserSession {
169    #[must_use]
170    pub fn active(&self) -> bool {
171        self.finished_at.is_none() && self.user.is_valid()
172    }
173}
174
175impl BrowserSession {
176    #[must_use]
177    pub fn samples(now: chrono::DateTime<Utc>, rng: &mut impl RngExt) -> Vec<Self> {
178        User::samples(now, rng)
179            .into_iter()
180            .map(|user| BrowserSession {
181                id: Ulid::from_datetime_with_rng(now, rng),
182                user,
183                created_at: now,
184                finished_at: None,
185                user_agent: Some(
186                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned()
187                ),
188                last_active_at: Some(now),
189                last_active_ip: None,
190            })
191            .collect()
192    }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
196pub struct UserEmail {
197    pub id: Ulid,
198    pub user_id: Ulid,
199    pub email: String,
200    pub created_at: DateTime<Utc>,
201}
202
203impl UserEmail {
204    #[must_use]
205    pub fn samples(now: chrono::DateTime<Utc>, rng: &mut impl RngExt) -> Vec<Self> {
206        vec![
207            Self {
208                id: Ulid::from_datetime_with_rng(now, rng),
209                user_id: Ulid::from_datetime_with_rng(now, rng),
210                email: "alice@example.com".to_owned(),
211                created_at: now,
212            },
213            Self {
214                id: Ulid::from_datetime_with_rng(now, rng),
215                user_id: Ulid::from_datetime_with_rng(now, rng),
216                email: "bob@example.com".to_owned(),
217                created_at: now,
218            },
219        ]
220    }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
224pub struct UserRegistrationPassword {
225    pub hashed_password: String,
226    pub version: u16,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
230pub struct UserRegistrationToken {
231    pub id: Ulid,
232    pub token: String,
233    /// A localpart to impose, or a template for one (see
234    /// [`is_username_template`]). A template token works only through its
235    /// `/register?token=…` link: the registration token step refuses it.
236    pub username: Option<String>,
237    pub email: Option<String>,
238    pub passwordless: bool,
239    pub room_id: Option<String>,
240    pub usage_limit: Option<u32>,
241    pub times_used: u32,
242    pub created_at: DateTime<Utc>,
243    pub last_used_at: Option<DateTime<Utc>>,
244    pub expires_at: Option<DateTime<Utc>>,
245    pub revoked_at: Option<DateTime<Utc>>,
246}
247
248impl UserRegistrationToken {
249    /// Returns `true` if the token is still valid and can be used
250    #[must_use]
251    pub fn is_valid(&self, now: DateTime<Utc>) -> bool {
252        // Check if revoked
253        if self.revoked_at.is_some() {
254            return false;
255        }
256
257        // Check if expired
258        if let Some(expires_at) = self.expires_at
259            && now >= expires_at
260        {
261            return false;
262        }
263
264        // Check if usage limit exceeded
265        if let Some(usage_limit) = self.usage_limit
266            && self.times_used >= usage_limit
267        {
268            return false;
269        }
270
271        true
272    }
273
274    /// Returns `true` if the token is an invite: it registers without a
275    /// password, as the username (or username template) and the email
276    /// address it pins
277    #[must_use]
278    pub fn is_invite(&self) -> bool {
279        self.passwordless && self.username.is_some() && self.email.is_some()
280    }
281
282    /// Returns `true` if the token can still be used (not expired and under
283    /// usage limit)
284    #[must_use]
285    pub fn can_be_used(&self, now: DateTime<Utc>) -> bool {
286        self.is_valid(now)
287    }
288
289    /// The username, if it is a template (see [`is_username_template`])
290    #[must_use]
291    pub fn username_template(&self) -> Option<&str> {
292        self.username
293            .as_deref()
294            .filter(|username| is_username_template(username))
295    }
296}
297
298/// Whether a registration token's `username` is a minijinja template,
299/// rendered with `random` when registering. Every minijinja delimiter starts
300/// with `{`, which the registration policy refuses in a localpart.
301#[must_use]
302pub fn is_username_template(username: &str) -> bool {
303    username.contains('{')
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
307pub struct UserRegistration {
308    pub id: Ulid,
309    pub username: String,
310    pub display_name: Option<String>,
311    pub terms_url: Option<Url>,
312    pub email_authentication_id: Option<Ulid>,
313    pub user_registration_token_id: Option<Ulid>,
314    pub password: Option<UserRegistrationPassword>,
315    pub upstream_oauth_authorization_session_id: Option<Ulid>,
316    pub post_auth_action: Option<serde_json::Value>,
317    pub ip_address: Option<IpAddr>,
318    pub user_agent: Option<String>,
319    pub created_at: DateTime<Utc>,
320    pub completed_at: Option<DateTime<Utc>>,
321}