Skip to main content

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