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);