mas_storage/user/
recovery.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 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 async_trait::async_trait;
11use mas_data_model::{Clock, UserEmail, UserRecoverySession, UserRecoveryTicket};
12use rand_core::RngCore;
13use ulid::Ulid;
14
15use crate::repository_impl;
16
17/// A [`UserRecoveryRepository`] helps interacting with [`UserRecoverySession`]
18/// and [`UserRecoveryTicket`] saved in the storage backend
19#[async_trait]
20pub trait UserRecoveryRepository: Send + Sync {
21    /// The error type returned by the repository
22    type Error;
23
24    /// Lookup an [`UserRecoverySession`] by its ID
25    ///
26    /// Returns `None` if no [`UserRecoverySession`] was found
27    ///
28    /// # Parameters
29    ///
30    /// * `id`: The ID of the [`UserRecoverySession`] to lookup
31    ///
32    /// # Errors
33    ///
34    /// Returns [`Self::Error`] if the underlying repository fails
35    async fn lookup_session(
36        &mut self,
37        id: Ulid,
38    ) -> Result<Option<UserRecoverySession>, Self::Error>;
39
40    /// Create a new [`UserRecoverySession`] for the given email
41    ///
42    /// Returns the newly created [`UserRecoverySession`]
43    ///
44    /// # Parameters
45    ///
46    /// * `rng`: The random number generator to use
47    /// * `clock`: The clock to use
48    /// * `email`: The email to create the session for
49    /// * `user_agent`: The user agent of the browser which initiated the
50    ///   session
51    /// * `ip_address`: The IP address of the browser which initiated the
52    ///   session, if known
53    /// * `locale`: The locale of the browser which initiated the session
54    ///
55    /// # Errors
56    ///
57    /// Returns [`Self::Error`] if the underlying repository fails
58    async fn add_session(
59        &mut self,
60        rng: &mut (dyn RngCore + Send),
61        clock: &dyn Clock,
62        email: String,
63        user_agent: String,
64        ip_address: Option<IpAddr>,
65        locale: String,
66    ) -> Result<UserRecoverySession, Self::Error>;
67
68    /// Find a [`UserRecoveryTicket`] by its ticket
69    ///
70    /// Returns `None` if no [`UserRecoveryTicket`] was found
71    ///
72    /// # Parameters
73    ///
74    /// * `ticket`: The ticket of the [`UserRecoveryTicket`] to lookup
75    ///
76    /// # Errors
77    ///
78    /// Returns [`Self::Error`] if the underlying repository fails
79    async fn find_ticket(
80        &mut self,
81        ticket: &str,
82    ) -> Result<Option<UserRecoveryTicket>, Self::Error>;
83
84    /// Add a [`UserRecoveryTicket`] to the given [`UserRecoverySession`] for
85    /// the given [`UserEmail`]
86    ///
87    /// # Parameters
88    ///
89    /// * `rng`: The random number generator to use
90    /// * `clock`: The clock to use
91    /// * `session`: The [`UserRecoverySession`] to add the ticket to
92    /// * `user_email`: The [`UserEmail`] to add the ticket for
93    /// * `ticket`: The ticket to add
94    ///
95    /// # Errors
96    ///
97    /// Returns [`Self::Error`] if the underlying repository fails
98    async fn add_ticket(
99        &mut self,
100        rng: &mut (dyn RngCore + Send),
101        clock: &dyn Clock,
102        user_recovery_session: &UserRecoverySession,
103        user_email: &UserEmail,
104        ticket: String,
105    ) -> Result<UserRecoveryTicket, Self::Error>;
106
107    /// Consume a [`UserRecoveryTicket`] and mark the session as used
108    ///
109    /// # Parameters
110    ///
111    /// * `clock`: The clock to use to record the time of consumption
112    /// * `ticket`: The [`UserRecoveryTicket`] to consume
113    /// * `session`: The [`UserRecoverySession`] to mark as used
114    ///
115    /// # Errors
116    ///
117    /// Returns [`Self::Error`] if the underlying repository fails or if the
118    /// recovery session was already used
119    async fn consume_ticket(
120        &mut self,
121        clock: &dyn Clock,
122        user_recovery_ticket: UserRecoveryTicket,
123        user_recovery_session: UserRecoverySession,
124    ) -> Result<UserRecoverySession, Self::Error>;
125
126    /// Cleanup old recovery sessions
127    ///
128    /// This will delete recovery sessions with IDs up to and including `until`.
129    /// Uses ULID cursor-based pagination for efficiency.
130    /// Tickets will cascade-delete automatically.
131    ///
132    /// Returns the number of sessions deleted and the cursor for the next batch
133    ///
134    /// # Parameters
135    ///
136    /// * `since`: The cursor to start from (exclusive), or `None` to start from
137    ///   the beginning
138    /// * `until`: The maximum ULID to delete (inclusive upper bound)
139    /// * `limit`: The maximum number of sessions to delete in this batch
140    ///
141    /// # Errors
142    ///
143    /// Returns [`Self::Error`] if the underlying repository fails
144    async fn cleanup(
145        &mut self,
146        since: Option<Ulid>,
147        until: Ulid,
148        limit: usize,
149    ) -> Result<(usize, Option<Ulid>), Self::Error>;
150}
151
152repository_impl!(UserRecoveryRepository:
153    async fn lookup_session(&mut self, id: Ulid) -> Result<Option<UserRecoverySession>, Self::Error>;
154
155    async fn add_session(
156        &mut self,
157        rng: &mut (dyn RngCore + Send),
158        clock: &dyn Clock,
159        email: String,
160        user_agent: String,
161        ip_address: Option<IpAddr>,
162        locale: String,
163    ) -> Result<UserRecoverySession, Self::Error>;
164
165    async fn find_ticket(
166        &mut self,
167        ticket: &str,
168    ) -> Result<Option<UserRecoveryTicket>, Self::Error>;
169
170    async fn add_ticket(
171        &mut self,
172        rng: &mut (dyn RngCore + Send),
173        clock: &dyn Clock,
174        user_recovery_session: &UserRecoverySession,
175        user_email: &UserEmail,
176        ticket: String,
177    ) -> Result<UserRecoveryTicket, Self::Error>;
178
179    async fn consume_ticket(
180        &mut self,
181        clock: &dyn Clock,
182        user_recovery_ticket: UserRecoveryTicket,
183        user_recovery_session: UserRecoverySession,
184    ) -> Result<UserRecoverySession, Self::Error>;
185
186    async fn cleanup(
187        &mut self,
188        since: Option<Ulid>,
189        until: Ulid,
190        limit: usize,
191    ) -> Result<(usize, Option<Ulid>), Self::Error>;
192);