mas_handlers/graphql/model/
browser_sessions.rs1use 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#[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 pub async fn id(&self) -> ID {
37 NodeType::BrowserSession.id(self.0.id)
38 }
39
40 async fn user(&self) -> User {
42 User(self.0.user.clone())
43 }
44
45 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 pub async fn created_at(&self) -> DateTime<Utc> {
65 self.0.created_at
66 }
67
68 pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
70 self.0.finished_at
71 }
72
73 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 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 pub async fn last_active_ip(&self) -> Option<String> {
93 self.0.last_active_ip.map(|ip| ip.to_string())
94 }
95
96 pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
98 self.0.last_active_at
99 }
100
101 #[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#[derive(Description)]
196pub struct Authentication(pub mas_data_model::Authentication);
197
198#[Object(use_type_description)]
199impl Authentication {
200 pub async fn id(&self) -> ID {
202 NodeType::Authentication.id(self.0.id)
203 }
204
205 pub async fn created_at(&self) -> DateTime<Utc> {
207 self.0.created_at
208 }
209}