mas_handlers/graphql/model/
browser_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 async_graphql::{
8    Context, Description, ID, Object,
9    connection::{Connection, Edge, OpaqueCursor, query},
10};
11use chrono::{DateTime, Utc};
12use mas_data_model::Device;
13use mas_storage::{
14    Pagination, RepositoryAccess, app_session::AppSessionFilter, user::BrowserSessionRepository,
15};
16
17use super::{
18    AppSession, CompatSession, Cursor, NodeCursor, NodeType, OAuth2Session, PreloadedTotalCount,
19    SessionState, User, UserAgent,
20};
21use crate::graphql::state::ContextExt;
22
23/// A browser session represents a logged in user in a browser.
24#[derive(Description)]
25pub struct BrowserSession(pub mas_data_model::BrowserSession);
26
27impl From<mas_data_model::BrowserSession> for BrowserSession {
28    fn from(v: mas_data_model::BrowserSession) -> Self {
29        Self(v)
30    }
31}
32
33#[Object(use_type_description)]
34impl BrowserSession {
35    /// ID of the object.
36    pub async fn id(&self) -> ID {
37        NodeType::BrowserSession.id(self.0.id)
38    }
39
40    /// The user logged in this session.
41    async fn user(&self) -> User {
42        User(self.0.user.clone())
43    }
44
45    /// The most recent authentication of this session.
46    async fn last_authentication(
47        &self,
48        ctx: &Context<'_>,
49    ) -> Result<Option<Authentication>, async_graphql::Error> {
50        let state = ctx.state();
51        let mut repo = state.repository().await?;
52
53        let last_authentication = repo
54            .browser_session()
55            .get_last_authentication(&self.0)
56            .await?;
57
58        repo.cancel().await?;
59
60        Ok(last_authentication.map(Authentication))
61    }
62
63    /// When the object was created.
64    pub async fn created_at(&self) -> DateTime<Utc> {
65        self.0.created_at
66    }
67
68    /// When the session was finished.
69    pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
70        self.0.finished_at
71    }
72
73    /// The state of the session.
74    pub async fn state(&self) -> SessionState {
75        if self.0.finished_at.is_some() {
76            SessionState::Finished
77        } else {
78            SessionState::Active
79        }
80    }
81
82    /// The user-agent with which the session was created.
83    pub async fn user_agent(&self) -> Option<UserAgent> {
84        self.0
85            .user_agent
86            .clone()
87            .map(mas_data_model::UserAgent::parse)
88            .map(UserAgent::from)
89    }
90
91    /// The last IP address used by the session.
92    pub async fn last_active_ip(&self) -> Option<String> {
93        self.0.last_active_ip.map(|ip| ip.to_string())
94    }
95
96    /// The last time the session was active.
97    pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
98        self.0.last_active_at
99    }
100
101    /// Get the list of both compat and OAuth 2.0 sessions started by this
102    /// browser session, chronologically sorted
103    #[allow(clippy::too_many_arguments)]
104    async fn app_sessions(
105        &self,
106        ctx: &Context<'_>,
107
108        #[graphql(name = "state", desc = "List only sessions in the given state.")]
109        state_param: Option<SessionState>,
110
111        #[graphql(name = "device", desc = "List only sessions for the given device.")]
112        device_param: Option<String>,
113
114        #[graphql(desc = "Returns the elements in the list that come after the cursor.")]
115        after: Option<String>,
116        #[graphql(desc = "Returns the elements in the list that come before the cursor.")]
117        before: Option<String>,
118        #[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
119        #[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
120    ) -> Result<Connection<Cursor, AppSession, PreloadedTotalCount>, async_graphql::Error> {
121        let state = ctx.state();
122        let mut repo = state.repository().await?;
123
124        query(
125            after,
126            before,
127            first,
128            last,
129            async |after, before, first, last| {
130                let after_id = after
131                    .map(|x: OpaqueCursor<NodeCursor>| {
132                        x.extract_for_types(&[NodeType::OAuth2Session, NodeType::CompatSession])
133                    })
134                    .transpose()?;
135                let before_id = before
136                    .map(|x: OpaqueCursor<NodeCursor>| {
137                        x.extract_for_types(&[NodeType::OAuth2Session, NodeType::CompatSession])
138                    })
139                    .transpose()?;
140                let pagination = Pagination::try_new(before_id, after_id, first, last)?;
141
142                let device_param = device_param.map(Device::try_from).transpose()?;
143
144                let filter = AppSessionFilter::new().for_browser_session(&self.0);
145
146                let filter = match state_param {
147                    Some(SessionState::Active) => filter.active_only(),
148                    Some(SessionState::Finished) => filter.finished_only(),
149                    None => filter,
150                };
151
152                let filter = match device_param.as_ref() {
153                    Some(device) => filter.for_device(device),
154                    None => filter,
155                };
156
157                let page = repo.app_session().list(filter, pagination).await?;
158
159                let count = if ctx.look_ahead().field("totalCount").exists() {
160                    Some(repo.app_session().count(filter).await?)
161                } else {
162                    None
163                };
164
165                repo.cancel().await?;
166
167                let mut connection = Connection::with_additional_fields(
168                    page.has_previous_page,
169                    page.has_next_page,
170                    PreloadedTotalCount(count),
171                );
172
173                connection
174                    .edges
175                    .extend(page.edges.into_iter().map(|s| match s {
176                        mas_storage::app_session::AppSession::Compat(session) => Edge::new(
177                            OpaqueCursor(NodeCursor(NodeType::CompatSession, session.id)),
178                            AppSession::CompatSession(Box::new(CompatSession::new(*session))),
179                        ),
180                        mas_storage::app_session::AppSession::OAuth2(session) => Edge::new(
181                            OpaqueCursor(NodeCursor(NodeType::OAuth2Session, session.id)),
182                            AppSession::OAuth2Session(Box::new(OAuth2Session(*session))),
183                        ),
184                    }));
185
186                Ok::<_, async_graphql::Error>(connection)
187            },
188        )
189        .await
190    }
191}
192
193/// An authentication records when a user enter their credential in a browser
194/// session.
195#[derive(Description)]
196pub struct Authentication(pub mas_data_model::Authentication);
197
198#[Object(use_type_description)]
199impl Authentication {
200    /// ID of the object.
201    pub async fn id(&self) -> ID {
202        NodeType::Authentication.id(self.0.id)
203    }
204
205    /// When the object was created.
206    pub async fn created_at(&self) -> DateTime<Utc> {
207        self.0.created_at
208    }
209}