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}