mas_data_model/compat/
sso_login.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE 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::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        session_id: Ulid,
22    },
23    Exchanged {
24        fulfilled_at: DateTime<Utc>,
25        exchanged_at: DateTime<Utc>,
26        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 session ID associated with the login.
84    ///
85    /// Returns `None` if the compat SSO login state is [`Pending`].
86    ///
87    /// [`Pending`]: CompatSsoLoginState::Pending
88    #[must_use]
89    pub fn session_id(&self) -> Option<Ulid> {
90        match self {
91            Self::Pending => None,
92            Self::Fulfilled { session_id, .. } | Self::Exchanged { session_id, .. } => {
93                Some(*session_id)
94            }
95        }
96    }
97
98    /// Transition the compat SSO login state from [`Pending`] to [`Fulfilled`].
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the compat SSO login state is not [`Pending`].
103    ///
104    /// [`Pending`]: CompatSsoLoginState::Pending
105    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
106    pub fn fulfill(
107        self,
108        fulfilled_at: DateTime<Utc>,
109        session: &CompatSession,
110    ) -> Result<Self, InvalidTransitionError> {
111        match self {
112            Self::Pending => Ok(Self::Fulfilled {
113                fulfilled_at,
114                session_id: session.id,
115            }),
116            Self::Fulfilled { .. } | Self::Exchanged { .. } => Err(InvalidTransitionError),
117        }
118    }
119
120    /// Transition the compat SSO login state from [`Fulfilled`] to
121    /// [`Exchanged`].
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if the compat SSO login state is not [`Fulfilled`].
126    ///
127    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
128    /// [`Exchanged`]: CompatSsoLoginState::Exchanged
129    pub fn exchange(self, exchanged_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
130        match self {
131            Self::Fulfilled {
132                fulfilled_at,
133                session_id,
134            } => Ok(Self::Exchanged {
135                fulfilled_at,
136                exchanged_at,
137                session_id,
138            }),
139            Self::Pending { .. } | Self::Exchanged { .. } => Err(InvalidTransitionError),
140        }
141    }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
145pub struct CompatSsoLogin {
146    pub id: Ulid,
147    pub redirect_uri: Url,
148    pub login_token: String,
149    pub created_at: DateTime<Utc>,
150    pub state: CompatSsoLoginState,
151}
152
153impl std::ops::Deref for CompatSsoLogin {
154    type Target = CompatSsoLoginState;
155
156    fn deref(&self) -> &Self::Target {
157        &self.state
158    }
159}
160
161impl CompatSsoLogin {
162    /// Transition the compat SSO login from a [`Pending`] state to
163    /// [`Fulfilled`].
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if the compat SSO login state is not [`Pending`].
168    ///
169    /// [`Pending`]: CompatSsoLoginState::Pending
170    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
171    pub fn fulfill(
172        mut self,
173        fulfilled_at: DateTime<Utc>,
174        session: &CompatSession,
175    ) -> Result<Self, InvalidTransitionError> {
176        self.state = self.state.fulfill(fulfilled_at, session)?;
177        Ok(self)
178    }
179
180    /// Transition the compat SSO login from a [`Fulfilled`] state to
181    /// [`Exchanged`].
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if the compat SSO login state is not [`Fulfilled`].
186    ///
187    /// [`Fulfilled`]: CompatSsoLoginState::Fulfilled
188    /// [`Exchanged`]: CompatSsoLoginState::Exchanged
189    pub fn exchange(mut self, exchanged_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
190        self.state = self.state.exchange(exchanged_at)?;
191        Ok(self)
192    }
193}