mas_storage/compat/
session.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2023, 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 chrono::{DateTime, Utc};
12use mas_data_model::{BrowserSession, Clock, CompatSession, CompatSsoLogin, Device, User};
13use rand_core::RngCore;
14use ulid::Ulid;
15
16use crate::{Page, Pagination, repository_impl, user::BrowserSessionFilter};
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum CompatSessionState {
20    Active,
21    Finished,
22}
23
24impl CompatSessionState {
25    /// Returns [`true`] if we're looking for active sessions
26    #[must_use]
27    pub fn is_active(self) -> bool {
28        matches!(self, Self::Active)
29    }
30
31    /// Returns [`true`] if we're looking for finished sessions
32    #[must_use]
33    pub fn is_finished(self) -> bool {
34        matches!(self, Self::Finished)
35    }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum CompatSessionType {
40    SsoLogin,
41    Unknown,
42}
43
44impl CompatSessionType {
45    /// Returns [`true`] if we're looking for SSO logins
46    #[must_use]
47    pub fn is_sso_login(self) -> bool {
48        matches!(self, Self::SsoLogin)
49    }
50
51    /// Returns [`true`] if we're looking for unknown sessions
52    #[must_use]
53    pub fn is_unknown(self) -> bool {
54        matches!(self, Self::Unknown)
55    }
56}
57
58/// Filter parameters for listing compatibility sessions
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
60pub struct CompatSessionFilter<'a> {
61    user: Option<&'a User>,
62    browser_session: Option<&'a BrowserSession>,
63    browser_session_filter: Option<BrowserSessionFilter<'a>>,
64    state: Option<CompatSessionState>,
65    auth_type: Option<CompatSessionType>,
66    device: Option<&'a Device>,
67    last_active_before: Option<DateTime<Utc>>,
68    last_active_after: Option<DateTime<Utc>>,
69}
70
71impl<'a> CompatSessionFilter<'a> {
72    /// Create a new [`CompatSessionFilter`] with default values
73    #[must_use]
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Set the user who owns the compatibility sessions
79    #[must_use]
80    pub fn for_user(mut self, user: &'a User) -> Self {
81        self.user = Some(user);
82        self
83    }
84
85    /// Get the user filter
86    #[must_use]
87    pub fn user(&self) -> Option<&'a User> {
88        self.user
89    }
90
91    /// Set the device filter
92    #[must_use]
93    pub fn for_device(mut self, device: &'a Device) -> Self {
94        self.device = Some(device);
95        self
96    }
97
98    /// Get the device filter
99    #[must_use]
100    pub fn device(&self) -> Option<&'a Device> {
101        self.device
102    }
103
104    /// Set the browser session filter
105    #[must_use]
106    pub fn for_browser_session(mut self, browser_session: &'a BrowserSession) -> Self {
107        self.browser_session = Some(browser_session);
108        self
109    }
110
111    /// Set the browser sessions filter
112    #[must_use]
113    pub fn for_browser_sessions(
114        mut self,
115        browser_session_filter: BrowserSessionFilter<'a>,
116    ) -> Self {
117        self.browser_session_filter = Some(browser_session_filter);
118        self
119    }
120
121    /// Get the browser session filter
122    #[must_use]
123    pub fn browser_session(&self) -> Option<&'a BrowserSession> {
124        self.browser_session
125    }
126
127    /// Get the browser sessions filter
128    #[must_use]
129    pub fn browser_session_filter(&self) -> Option<BrowserSessionFilter<'a>> {
130        self.browser_session_filter
131    }
132
133    /// Only return sessions with a last active time before the given time
134    #[must_use]
135    pub fn with_last_active_before(mut self, last_active_before: DateTime<Utc>) -> Self {
136        self.last_active_before = Some(last_active_before);
137        self
138    }
139
140    /// Only return sessions with a last active time after the given time
141    #[must_use]
142    pub fn with_last_active_after(mut self, last_active_after: DateTime<Utc>) -> Self {
143        self.last_active_after = Some(last_active_after);
144        self
145    }
146
147    /// Get the last active before filter
148    ///
149    /// Returns [`None`] if no client filter was set
150    #[must_use]
151    pub fn last_active_before(&self) -> Option<DateTime<Utc>> {
152        self.last_active_before
153    }
154
155    /// Get the last active after filter
156    ///
157    /// Returns [`None`] if no client filter was set
158    #[must_use]
159    pub fn last_active_after(&self) -> Option<DateTime<Utc>> {
160        self.last_active_after
161    }
162
163    /// Only return active compatibility sessions
164    #[must_use]
165    pub fn active_only(mut self) -> Self {
166        self.state = Some(CompatSessionState::Active);
167        self
168    }
169
170    /// Only return finished compatibility sessions
171    #[must_use]
172    pub fn finished_only(mut self) -> Self {
173        self.state = Some(CompatSessionState::Finished);
174        self
175    }
176
177    /// Get the state filter
178    #[must_use]
179    pub fn state(&self) -> Option<CompatSessionState> {
180        self.state
181    }
182
183    /// Only return SSO login compatibility sessions
184    #[must_use]
185    pub fn sso_login_only(mut self) -> Self {
186        self.auth_type = Some(CompatSessionType::SsoLogin);
187        self
188    }
189
190    /// Only return unknown compatibility sessions
191    #[must_use]
192    pub fn unknown_only(mut self) -> Self {
193        self.auth_type = Some(CompatSessionType::Unknown);
194        self
195    }
196
197    /// Get the auth type filter
198    #[must_use]
199    pub fn auth_type(&self) -> Option<CompatSessionType> {
200        self.auth_type
201    }
202}
203
204/// A [`CompatSessionRepository`] helps interacting with
205/// [`CompatSession`] saved in the storage backend
206#[async_trait]
207pub trait CompatSessionRepository: Send + Sync {
208    /// The error type returned by the repository
209    type Error;
210
211    /// Lookup a compat session by its ID
212    ///
213    /// Returns the compat session if it exists, `None` otherwise
214    ///
215    /// # Parameters
216    ///
217    /// * `id`: The ID of the compat session to lookup
218    ///
219    /// # Errors
220    ///
221    /// Returns [`Self::Error`] if the underlying repository fails
222    async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSession>, Self::Error>;
223
224    /// Start a new compat session
225    ///
226    /// Returns the newly created compat session
227    ///
228    /// # Parameters
229    ///
230    /// * `rng`: The random number generator to use
231    /// * `clock`: The clock used to generate timestamps
232    /// * `user`: The user to create the compat session for
233    /// * `device`: The device ID of this session
234    /// * `browser_session`: The browser session which created this session
235    /// * `is_synapse_admin`: Whether the session is a synapse admin session
236    /// * `human_name`: The human-readable name of the session provided by the
237    ///   client or the user
238    ///
239    /// # Errors
240    ///
241    /// Returns [`Self::Error`] if the underlying repository fails
242    #[expect(clippy::too_many_arguments)]
243    async fn add(
244        &mut self,
245        rng: &mut (dyn RngCore + Send),
246        clock: &dyn Clock,
247        user: &User,
248        device: Device,
249        browser_session: Option<&BrowserSession>,
250        is_synapse_admin: bool,
251        human_name: Option<String>,
252    ) -> Result<CompatSession, Self::Error>;
253
254    /// End a compat session
255    ///
256    /// Returns the ended compat session
257    ///
258    /// # Parameters
259    ///
260    /// * `clock`: The clock used to generate timestamps
261    /// * `compat_session`: The compat session to end
262    ///
263    /// # Errors
264    ///
265    /// Returns [`Self::Error`] if the underlying repository fails
266    async fn finish(
267        &mut self,
268        clock: &dyn Clock,
269        compat_session: CompatSession,
270    ) -> Result<CompatSession, Self::Error>;
271
272    /// Mark all the [`CompatSession`] matching the given filter as finished
273    ///
274    /// Returns the number of sessions affected
275    ///
276    /// # Parameters
277    ///
278    /// * `clock`: The clock used to generate timestamps
279    /// * `filter`: The filter to apply
280    ///
281    /// # Errors
282    ///
283    /// Returns [`Self::Error`] if the underlying repository fails
284    async fn finish_bulk(
285        &mut self,
286        clock: &dyn Clock,
287        filter: CompatSessionFilter<'_>,
288    ) -> Result<usize, Self::Error>;
289
290    /// List [`CompatSession`] with the given filter and pagination
291    ///
292    /// Returns a page of compat sessions, with the associated SSO logins if any
293    ///
294    /// # Parameters
295    ///
296    /// * `filter`: The filter to apply
297    /// * `pagination`: The pagination parameters
298    ///
299    /// # Errors
300    ///
301    /// Returns [`Self::Error`] if the underlying repository fails
302    async fn list(
303        &mut self,
304        filter: CompatSessionFilter<'_>,
305        pagination: Pagination,
306    ) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
307
308    /// Count the number of [`CompatSession`] with the given filter
309    ///
310    /// # Parameters
311    ///
312    /// * `filter`: The filter to apply
313    ///
314    /// # Errors
315    ///
316    /// Returns [`Self::Error`] if the underlying repository fails
317    async fn count(&mut self, filter: CompatSessionFilter<'_>) -> Result<usize, Self::Error>;
318
319    /// Record a batch of [`CompatSession`] activity
320    ///
321    /// # Parameters
322    ///
323    /// * `activity`: A list of tuples containing the session ID, the last
324    ///   activity timestamp and the IP address of the client
325    ///
326    /// # Errors
327    ///
328    /// Returns [`Self::Error`] if the underlying repository fails
329    async fn record_batch_activity(
330        &mut self,
331        activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
332    ) -> Result<(), Self::Error>;
333
334    /// Record the user agent of a compat session
335    ///
336    /// # Parameters
337    ///
338    /// * `compat_session`: The compat session to record the user agent for
339    /// * `user_agent`: The user agent to record
340    ///
341    /// # Errors
342    ///
343    /// Returns [`Self::Error`] if the underlying repository fails
344    async fn record_user_agent(
345        &mut self,
346        compat_session: CompatSession,
347        user_agent: String,
348    ) -> Result<CompatSession, Self::Error>;
349
350    /// Set the human name of a compat session
351    ///
352    /// # Parameters
353    ///
354    /// * `compat_session`: The compat session to set the human name for
355    /// * `human_name`: The human name to set
356    ///
357    /// # Errors
358    ///
359    /// Returns [`Self::Error`] if the underlying repository fails
360    async fn set_human_name(
361        &mut self,
362        compat_session: CompatSession,
363        human_name: Option<String>,
364    ) -> Result<CompatSession, Self::Error>;
365
366    /// Cleanup finished [`CompatSession`]s and their associated tokens.
367    ///
368    /// This deletes compat sessions that have been finished, along with their
369    /// associated access tokens, refresh tokens, and SSO logins.
370    ///
371    /// Returns the number of sessions deleted and the timestamp of the last
372    /// deleted session's `finished_at`, which can be used for pagination.
373    ///
374    /// # Parameters
375    ///
376    /// * `since`: Only delete sessions finished at or after this timestamp
377    /// * `until`: Only delete sessions finished before this timestamp
378    /// * `limit`: Maximum number of sessions to delete
379    ///
380    /// # Errors
381    ///
382    /// Returns [`Self::Error`] if the underlying repository fails
383    async fn cleanup_finished(
384        &mut self,
385        since: Option<DateTime<Utc>>,
386        until: DateTime<Utc>,
387        limit: usize,
388    ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
389
390    /// Clear IP addresses from sessions inactive since the threshold
391    ///
392    /// Sets `last_active_ip` to `NULL` for sessions where `last_active_at` is
393    /// before the threshold. Returns the number of sessions affected and the
394    /// last `last_active_at` timestamp processed for pagination.
395    ///
396    /// # Parameters
397    ///
398    /// * `since`: Only process sessions with `last_active_at` at or after this
399    ///   timestamp (exclusive). If `None`, starts from the beginning.
400    /// * `threshold`: Clear IPs for sessions with `last_active_at` before this
401    ///   time
402    /// * `limit`: Maximum number of sessions to update in this batch
403    ///
404    /// # Errors
405    ///
406    /// Returns [`Self::Error`] if the underlying repository fails
407    async fn cleanup_inactive_ips(
408        &mut self,
409        since: Option<DateTime<Utc>>,
410        threshold: DateTime<Utc>,
411        limit: usize,
412    ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
413}
414
415repository_impl!(CompatSessionRepository:
416    async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSession>, Self::Error>;
417
418    async fn add(
419        &mut self,
420        rng: &mut (dyn RngCore + Send),
421        clock: &dyn Clock,
422        user: &User,
423        device: Device,
424        browser_session: Option<&BrowserSession>,
425        is_synapse_admin: bool,
426        human_name: Option<String>,
427    ) -> Result<CompatSession, Self::Error>;
428
429    async fn finish(
430        &mut self,
431        clock: &dyn Clock,
432        compat_session: CompatSession,
433    ) -> Result<CompatSession, Self::Error>;
434
435    async fn finish_bulk(
436        &mut self,
437        clock: &dyn Clock,
438        filter: CompatSessionFilter<'_>,
439    ) -> Result<usize, Self::Error>;
440
441    async fn list(
442        &mut self,
443        filter: CompatSessionFilter<'_>,
444        pagination: Pagination,
445    ) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
446
447    async fn count(&mut self, filter: CompatSessionFilter<'_>) -> Result<usize, Self::Error>;
448
449    async fn record_batch_activity(
450        &mut self,
451        activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
452    ) -> Result<(), Self::Error>;
453
454    async fn record_user_agent(
455        &mut self,
456        compat_session: CompatSession,
457        user_agent: String,
458    ) -> Result<CompatSession, Self::Error>;
459
460    async fn set_human_name(
461        &mut self,
462        compat_session: CompatSession,
463        human_name: Option<String>,
464    ) -> Result<CompatSession, Self::Error>;
465
466    async fn cleanup_finished(
467        &mut self,
468        since: Option<DateTime<Utc>>,
469        until: DateTime<Utc>,
470        limit: usize,
471    ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
472
473    async fn cleanup_inactive_ips(
474        &mut self,
475        since: Option<DateTime<Utc>>,
476        threshold: DateTime<Utc>,
477        limit: usize,
478    ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
479);