mas_handlers/graphql/model/
compat_sessions.rs1use 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#[derive(Clone, Debug, Default)]
21enum ReverseReference<T> {
22 Loaded(T),
23 #[default]
24 Lazy,
25}
26
27#[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 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#[derive(Enum, Copy, Clone, Eq, PartialEq)]
55pub enum CompatSessionType {
56 SsoLogin,
58
59 Unknown,
61}
62
63#[Object(use_type_description)]
64impl CompatSession {
65 pub async fn id(&self) -> ID {
67 NodeType::CompatSession.id(self.session.id)
68 }
69
70 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 async fn device_id(&self) -> Option<&str> {
86 self.session.device.as_ref().map(Device::as_str)
87 }
88
89 pub async fn created_at(&self) -> DateTime<Utc> {
91 self.session.created_at
92 }
93
94 pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
96 self.session.finished_at()
97 }
98
99 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 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 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 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 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 pub async fn last_active_ip(&self) -> Option<String> {
161 self.session.last_active_ip.map(|ip| ip.to_string())
162 }
163
164 pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
166 self.session.last_active_at
167 }
168}
169
170#[derive(Description)]
173pub struct CompatSsoLogin(pub mas_data_model::CompatSsoLogin);
174
175#[Object(use_type_description)]
176impl CompatSsoLogin {
177 pub async fn id(&self) -> ID {
179 NodeType::CompatSsoLogin.id(self.0.id)
180 }
181
182 pub async fn created_at(&self) -> DateTime<Utc> {
184 self.0.created_at
185 }
186
187 async fn redirect_uri(&self) -> &Url {
189 &self.0.redirect_uri
190 }
191
192 async fn fulfilled_at(&self) -> Option<DateTime<Utc>> {
195 self.0.fulfilled_at()
196 }
197
198 async fn exchanged_at(&self) -> Option<DateTime<Utc>> {
200 self.0.exchanged_at()
201 }
202
203 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}