mas_data_model/compat/
sso_login.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2023, 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 chrono::{DateTime, Utc};
8use serde::Serialize;
9use ulid::Ulid;
10use url::Url;
11
12use super::CompatSession;
13use crate::{BrowserSession, InvalidTransitionError};
14
15#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
16pub enum CompatSsoLoginState {
17    #[default]
18    Pending,
19    Fulfilled {
20        fulfilled_at: DateTime<Utc>,
21        browser_session_id: Ulid,
22    },
23    Exchanged {
24        fulfilled_at: DateTime<Utc>,
25        exchanged_at: DateTime<Utc>,
26        compat_session_id: Ulid,
27    },
28}
29
30impl CompatSsoLoginState {
31    /// Returns `true` if the compat SSO login state is [`Pending`].
32    ///
33    /// [`Pending`]: CompatSsoLoginState::Pending
34    #[must_use]
35    pub fn is_pending(&self) -> bool {
36        matches!(self, Self::Pending)
37    }
38
39    /// Returns `true` if the compat SSO login state is [`Fulfilled`].
40    ///
41    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
42    #[must_use]
43    pub fn is_fulfilled(&self) -> bool {
44        matches!(self, Self::Fulfilled { .. })
45    }
46
47    /// Returns `true` if the compat SSO login state is [`Exchanged`].
48    ///
49    /// [`Exchanged`]: CompatSsoLoginState::Exchanged
50    #[must_use]
51    pub fn is_exchanged(&self) -> bool {
52        matches!(self, Self::Exchanged { .. })
53    }
54
55    /// Get the time at which the login was fulfilled.
56    ///
57    /// Returns `None` if the compat SSO login state is [`Pending`].
58    ///
59    /// [`Pending`]: CompatSsoLoginState::Pending
60    #[must_use]
61    pub fn fulfilled_at(&self) -> Option<DateTime<Utc>> {
62        match self {
63            Self::Pending => None,
64            Self::Fulfilled { fulfilled_at, .. } | Self::Exchanged { fulfilled_at, .. } => {
65                Some(*fulfilled_at)
66            }
67        }
68    }
69
70    /// Get the time at which the login was exchanged.
71    ///
72    /// Returns `None` if the compat SSO login state is not [`Exchanged`].
73    ///
74    /// [`Exchanged`]: CompatSsoLoginState::Exchanged
75    #[must_use]
76    pub fn exchanged_at(&self) -> Option<DateTime<Utc>> {
77        match self {
78            Self::Pending | Self::Fulfilled { .. } => None,
79            Self::Exchanged { exchanged_at, .. } => Some(*exchanged_at),
80        }
81    }
82
83    /// Get the compat session ID associated with the login.
84    ///
85    /// Returns `None` if the compat SSO login state is [`Pending`] or
86    /// [`Fulfilled`].
87    ///
88    /// [`Pending`]: CompatSsoLoginState::Pending
89    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
90    #[must_use]
91    pub fn session_id(&self) -> Option<Ulid> {
92        match self {
93            Self::Pending | Self::Fulfilled { .. } => None,
94            Self::Exchanged {
95                compat_session_id: session_id,
96                ..
97            } => Some(*session_id),
98        }
99    }
100
101    /// Transition the compat SSO login state from [`Pending`] to [`Fulfilled`].
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the compat SSO login state is not [`Pending`].
106    ///
107    /// [`Pending`]: CompatSsoLoginState::Pending
108    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
109    pub fn fulfill(
110        self,
111        fulfilled_at: DateTime<Utc>,
112        browser_session: &BrowserSession,
113    ) -> Result<Self, InvalidTransitionError> {
114        match self {
115            Self::Pending => Ok(Self::Fulfilled {
116                fulfilled_at,
117                browser_session_id: browser_session.id,
118            }),
119            Self::Fulfilled { .. } | Self::Exchanged { .. } => Err(InvalidTransitionError),
120        }
121    }
122
123    /// Transition the compat SSO login state from [`Fulfilled`] to
124    /// [`Exchanged`].
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if the compat SSO login state is not [`Fulfilled`].
129    ///
130    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
131    /// [`Exchanged`]: CompatSsoLoginState::Exchanged
132    pub fn exchange(
133        self,
134        exchanged_at: DateTime<Utc>,
135        compat_session: &CompatSession,
136    ) -> Result<Self, InvalidTransitionError> {
137        match self {
138            Self::Fulfilled {
139                fulfilled_at,
140                browser_session_id: _,
141            } => Ok(Self::Exchanged {
142                fulfilled_at,
143                exchanged_at,
144                compat_session_id: compat_session.id,
145            }),
146            Self::Pending { .. } | Self::Exchanged { .. } => Err(InvalidTransitionError),
147        }
148    }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
152pub struct CompatSsoLogin {
153    pub id: Ulid,
154    pub redirect_uri: Url,
155    pub login_token: String,
156    pub created_at: DateTime<Utc>,
157    pub state: CompatSsoLoginState,
158}
159
160impl std::ops::Deref for CompatSsoLogin {
161    type Target = CompatSsoLoginState;
162
163    fn deref(&self) -> &Self::Target {
164        &self.state
165    }
166}
167
168impl CompatSsoLogin {
169    /// Transition the compat SSO login from a [`Pending`] state to
170    /// [`Fulfilled`].
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the compat SSO login state is not [`Pending`].
175    ///
176    /// [`Pending`]: CompatSsoLoginState::Pending
177    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
178    pub fn fulfill(
179        mut self,
180        fulfilled_at: DateTime<Utc>,
181        browser_session: &BrowserSession,
182    ) -> Result<Self, InvalidTransitionError> {
183        self.state = self.state.fulfill(fulfilled_at, browser_session)?;
184        Ok(self)
185    }
186
187    /// Transition the compat SSO login from a [`Fulfilled`] state to
188    /// [`Exchanged`].
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the compat SSO login state is not [`Fulfilled`].
193    ///
194    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
195    /// [`Exchanged`]: CompatSsoLoginState::Exchanged
196    pub fn exchange(
197        mut self,
198        exchanged_at: DateTime<Utc>,
199        compat_session: &CompatSession,
200    ) -> Result<Self, InvalidTransitionError> {
201        self.state = self.state.exchange(exchanged_at, compat_session)?;
202        Ok(self)
203    }
204}