use anyhow::Context as _;
use async_graphql::{Context, Description, Enum, Object, ID};
use chrono::{DateTime, Utc};
use mas_storage::{compat::CompatSessionRepository, user::UserRepository};
use url::Url;
use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
use crate::graphql::state::ContextExt;
#[derive(Clone, Debug, Default)]
enum ReverseReference<T> {
Loaded(T),
#[default]
Lazy,
}
#[derive(Description)]
pub struct CompatSession {
session: mas_data_model::CompatSession,
sso_login: ReverseReference<Option<mas_data_model::CompatSsoLogin>>,
}
impl CompatSession {
pub fn new(session: mas_data_model::CompatSession) -> Self {
Self {
session,
sso_login: ReverseReference::Lazy,
}
}
pub fn with_loaded_sso_login(
mut self,
sso_login: Option<mas_data_model::CompatSsoLogin>,
) -> Self {
self.sso_login = ReverseReference::Loaded(sso_login);
self
}
}
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum CompatSessionType {
SsoLogin,
Unknown,
}
#[Object(use_type_description)]
impl CompatSession {
pub async fn id(&self) -> ID {
NodeType::CompatSession.id(self.session.id)
}
async fn user(&self, ctx: &Context<'_>) -> Result<User, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let user = repo
.user()
.lookup(self.session.user_id)
.await?
.context("Could not load user")?;
repo.cancel().await?;
Ok(User(user))
}
async fn device_id(&self) -> &str {
self.session.device.as_str()
}
pub async fn created_at(&self) -> DateTime<Utc> {
self.session.created_at
}
pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
self.session.finished_at()
}
pub async fn user_agent(&self) -> Option<UserAgent> {
self.session.user_agent.clone().map(UserAgent::from)
}
pub async fn sso_login(
&self,
ctx: &Context<'_>,
) -> Result<Option<CompatSsoLogin>, async_graphql::Error> {
if let ReverseReference::Loaded(sso_login) = &self.sso_login {
return Ok(sso_login.clone().map(CompatSsoLogin));
}
let state = ctx.state();
let mut repo = state.repository().await?;
let sso_login = repo
.compat_sso_login()
.find_for_session(&self.session)
.await
.context("Could not load SSO login")?;
repo.cancel().await?;
Ok(sso_login.map(CompatSsoLogin))
}
pub async fn browser_session(
&self,
ctx: &Context<'_>,
) -> Result<Option<BrowserSession>, async_graphql::Error> {
let Some(user_session_id) = self.session.user_session_id else {
return Ok(None);
};
let state = ctx.state();
let mut repo = state.repository().await?;
let browser_session = repo
.browser_session()
.lookup(user_session_id)
.await?
.context("Could not load browser session")?;
repo.cancel().await?;
Ok(Some(BrowserSession(browser_session)))
}
pub async fn state(&self) -> SessionState {
match &self.session.state {
mas_data_model::CompatSessionState::Valid => SessionState::Active,
mas_data_model::CompatSessionState::Finished { .. } => SessionState::Finished,
}
}
pub async fn last_active_ip(&self) -> Option<String> {
self.session.last_active_ip.map(|ip| ip.to_string())
}
pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
self.session.last_active_at
}
}
#[derive(Description)]
pub struct CompatSsoLogin(pub mas_data_model::CompatSsoLogin);
#[Object(use_type_description)]
impl CompatSsoLogin {
pub async fn id(&self) -> ID {
NodeType::CompatSsoLogin.id(self.0.id)
}
pub async fn created_at(&self) -> DateTime<Utc> {
self.0.created_at
}
async fn redirect_uri(&self) -> &Url {
&self.0.redirect_uri
}
async fn fulfilled_at(&self) -> Option<DateTime<Utc>> {
self.0.fulfilled_at()
}
async fn exchanged_at(&self) -> Option<DateTime<Utc>> {
self.0.exchanged_at()
}
async fn session(
&self,
ctx: &Context<'_>,
) -> Result<Option<CompatSession>, async_graphql::Error> {
let Some(session_id) = self.0.session_id() else {
return Ok(None);
};
let state = ctx.state();
let mut repo = state.repository().await?;
let session = repo
.compat_session()
.lookup(session_id)
.await?
.context("Could not load compat session")?;
repo.cancel().await?;
Ok(Some(
CompatSession::new(session).with_loaded_sso_login(Some(self.0.clone())),
))
}
}