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}