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    pub usage_limit: Option<u32>,
234    pub times_used: u32,
235    pub created_at: DateTime<Utc>,
236    pub last_used_at: Option<DateTime<Utc>>,
237    pub expires_at: Option<DateTime<Utc>>,
238    pub revoked_at: Option<DateTime<Utc>>,
239}
240
241impl UserRegistrationToken {
242    /// Returns `true` if the token is still valid and can be used
243    #[must_use]
244    pub fn is_valid(&self, now: DateTime<Utc>) -> bool {
245        // Check if revoked
246        if self.revoked_at.is_some() {
247            return false;
248        }
249
250        // Check if expired
251        if let Some(expires_at) = self.expires_at
252            && now >= expires_at
253        {
254            return false;
255        }
256
257        // Check if usage limit exceeded
258        if let Some(usage_limit) = self.usage_limit
259            && self.times_used >= usage_limit
260        {
261            return false;
262        }
263
264        true
265    }
266
267    /// Returns `true` if the token can still be used (not expired and under
268    /// usage limit)
269    #[must_use]
270    pub fn can_be_used(&self, now: DateTime<Utc>) -> bool {
271        self.is_valid(now)
272    }
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
276pub struct UserRegistration {
277    pub id: Ulid,
278    pub username: String,
279    pub display_name: Option<String>,
280    pub terms_url: Option<Url>,
281    pub email_authentication_id: Option<Ulid>,
282    pub user_registration_token_id: Option<Ulid>,
283    pub password: Option<UserRegistrationPassword>,
284    pub upstream_oauth_authorization_session_id: Option<Ulid>,
285    pub post_auth_action: Option<serde_json::Value>,
286    pub ip_address: Option<IpAddr>,
287    pub user_agent: Option<String>,
288    pub created_at: DateTime<Utc>,
289    pub completed_at: Option<DateTime<Utc>>,
290}