mas_handlers/graphql/model/
compat_sessions.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-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 anyhow::Context as _;
8use async_graphql::{Context, Description, Enum, ID, Object};
9use chrono::{DateTime, Utc};
10use mas_data_model::Device;
11use mas_storage::{compat::CompatSessionRepository, user::UserRepository};
12use url::Url;
13
14use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
15use crate::graphql::state::ContextExt;
16
17/// Lazy-loaded reverse reference.
18///
19/// XXX: maybe we want to stick that in a utility module
20#[derive(Clone, Debug, Default)]
21enum ReverseReference<T> {
22    Loaded(T),
23    #[default]
24    Lazy,
25}
26
27/// A compat session represents a client session which used the legacy Matrix
28/// login API.
29#[derive(Description)]
30pub struct CompatSession {
31    session: mas_data_model::CompatSession,
32    sso_login: ReverseReference<Option<mas_data_model::CompatSsoLogin>>,
33}
34
35impl CompatSession {
36    pub fn new(session: mas_data_model::CompatSession) -> Self {
37        Self {
38            session,
39            sso_login: ReverseReference::Lazy,
40        }
41    }
42
43    /// Save an eagerly loaded SSO login.
44    pub fn with_loaded_sso_login(
45        mut self,
46        sso_login: Option<mas_data_model::CompatSsoLogin>,
47    ) -> Self {
48        self.sso_login = ReverseReference::Loaded(sso_login);
49        self
50    }
51}
52
53/// The type of a compatibility session.
54#[derive(Enum, Copy, Clone, Eq, PartialEq)]
55pub enum CompatSessionType {
56    /// The session was created by a SSO login.
57    SsoLogin,
58
59    /// The session was created by an unknown method.
60    Unknown,
61}
62
63#[Object(use_type_description)]
64impl CompatSession {
65    /// ID of the object.
66    pub async fn id(&self) -> ID {
67        NodeType::CompatSession.id(self.session.id)
68    }
69
70    /// The user authorized for this session.
71    async fn user(&self, ctx: &Context<'_>) -> Result<User, async_graphql::Error> {
72        let state = ctx.state();
73        let mut repo = state.repository().await?;
74        let user = repo
75            .user()
76            .lookup(self.session.user_id)
77            .await?
78            .context("Could not load user")?;
79        repo.cancel().await?;
80
81        Ok(User(user))
82    }
83
84    /// The Matrix Device ID of this session.
85    async fn device_id(&self) -> Option<&str> {
86        self.session.device.as_ref().map(Device::as_str)
87    }
88
89    /// When the object was created.
90    pub async fn created_at(&self) -> DateTime<Utc> {
91        self.session.created_at
92    }
93
94    /// When the session ended.
95    pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
96        self.session.finished_at()
97    }
98
99    /// The user-agent with which the session was created.
100    pub async fn user_agent(&self) -> Option<UserAgent> {
101        self.session
102            .user_agent
103            .clone()
104            .map(mas_data_model::UserAgent::parse)
105            .map(UserAgent::from)
106    }
107
108    /// The associated SSO login, if any.
109    pub async fn sso_login(
110        &self,
111        ctx: &Context<'_>,
112    ) -> Result<Option<CompatSsoLogin>, async_graphql::Error> {
113        if let ReverseReference::Loaded(sso_login) = &self.sso_login {
114            return Ok(sso_login.clone().map(CompatSsoLogin));
115        }
116
117        // We need to load it on the fly
118        let state = ctx.state();
119        let mut repo = state.repository().await?;
120        let sso_login = repo
121            .compat_sso_login()
122            .find_for_session(&self.session)
123            .await
124            .context("Could not load SSO login")?;
125        repo.cancel().await?;
126
127        Ok(sso_login.map(CompatSsoLogin))
128    }
129
130    /// The browser session which started this session, if any.
131    pub async fn browser_session(
132        &self,
133        ctx: &Context<'_>,
134    ) -> Result<Option<BrowserSession>, async_graphql::Error> {
135        let Some(user_session_id) = self.session.user_session_id else {
136            return Ok(None);
137        };
138
139        let state = ctx.state();
140        let mut repo = state.repository().await?;
141        let browser_session = repo
142            .browser_session()
143            .lookup(user_session_id)
144            .await?
145            .context("Could not load browser session")?;
146        repo.cancel().await?;
147
148        Ok(Some(BrowserSession(browser_session)))
149    }
150
151    /// The state of the session.
152    pub async fn state(&self) -> SessionState {
153        match &self.session.state {
154            mas_data_model::CompatSessionState::Valid => SessionState::Active,
155            mas_data_model::CompatSessionState::Finished { .. } => SessionState::Finished,
156        }
157    }
158
159    /// The last IP address used by the session.
160    pub async fn last_active_ip(&self) -> Option<String> {
161        self.session.last_active_ip.map(|ip| ip.to_string())
162    }
163
164    /// The last time the session was active.
165    pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
166        self.session.last_active_at
167    }
168}
169
170/// A compat SSO login represents a login done through the legacy Matrix login
171/// API, via the `m.login.sso` login method.
172#[derive(Description)]
173pub struct CompatSsoLogin(pub mas_data_model::CompatSsoLogin);
174
175#[Object(use_type_description)]
176impl CompatSsoLogin {
177    /// ID of the object.
178    pub async fn id(&self) -> ID {
179        NodeType::CompatSsoLogin.id(self.0.id)
180    }
181
182    /// When the object was created.
183    pub async fn created_at(&self) -> DateTime<Utc> {
184        self.0.created_at
185    }
186
187    /// The redirect URI used during the login.
188    async fn redirect_uri(&self) -> &Url {
189        &self.0.redirect_uri
190    }
191
192    /// When the login was fulfilled, and the user was redirected back to the
193    /// client.
194    async fn fulfilled_at(&self) -> Option<DateTime<Utc>> {
195        self.0.fulfilled_at()
196    }
197
198    /// When the client exchanged the login token sent during the redirection.
199    async fn exchanged_at(&self) -> Option<DateTime<Utc>> {
200        self.0.exchanged_at()
201    }
202
203    /// The compat session which was started by this login.
204    async fn session(
205        &self,
206        ctx: &Context<'_>,
207    ) -> Result<Option<CompatSession>, async_graphql::Error> {
208        let Some(session_id) = self.0.session_id() else {
209            return Ok(None);
210        };
211
212        let state = ctx.state();
213        let mut repo = state.repository().await?;
214        let session = repo
215            .compat_session()
216            .lookup(session_id)
217            .await?
218            .context("Could not load compat session")?;
219        repo.cancel().await?;
220
221        Ok(Some(
222            CompatSession::new(session).with_loaded_sso_login(Some(self.0.clone())),
223        ))
224    }
225}