use anyhow::Context as _;
use async_graphql::{Context, Description, Enum, Object, ID};
use chrono::{DateTime, Utc};
use mas_storage::{oauth2::OAuth2ClientRepository, user::BrowserSessionRepository};
use oauth2_types::{oidc::ApplicationType, scope::Scope};
use ulid::Ulid;
use url::Url;
use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
use crate::graphql::{state::ContextExt, UserId};
#[derive(Description)]
pub struct OAuth2Session(pub mas_data_model::Session);
#[Object(use_type_description)]
impl OAuth2Session {
pub async fn id(&self) -> ID {
NodeType::OAuth2Session.id(self.0.id)
}
pub async fn client(&self, ctx: &Context<'_>) -> Result<OAuth2Client, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let client = repo
.oauth2_client()
.lookup(self.0.client_id)
.await?
.context("Could not load client")?;
repo.cancel().await?;
Ok(OAuth2Client(client))
}
pub async fn scope(&self) -> String {
self.0.scope.to_string()
}
pub async fn created_at(&self) -> DateTime<Utc> {
self.0.created_at
}
pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
match &self.0.state {
mas_data_model::SessionState::Valid => None,
mas_data_model::SessionState::Finished { finished_at } => Some(*finished_at),
}
}
pub async fn user_agent(&self) -> Option<UserAgent> {
self.0.user_agent.clone().map(UserAgent::from)
}
pub async fn state(&self) -> SessionState {
match &self.0.state {
mas_data_model::SessionState::Valid => SessionState::Active,
mas_data_model::SessionState::Finished { .. } => SessionState::Finished,
}
}
pub async fn browser_session(
&self,
ctx: &Context<'_>,
) -> Result<Option<BrowserSession>, async_graphql::Error> {
let Some(user_session_id) = self.0.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 user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
let state = ctx.state();
let Some(user_id) = self.0.user_id else {
return Ok(None);
};
if !ctx.requester().is_owner_or_admin(&UserId(user_id)) {
return Err(async_graphql::Error::new("Unauthorized"));
}
let mut repo = state.repository().await?;
let user = repo
.user()
.lookup(user_id)
.await?
.context("Could not load user")?;
repo.cancel().await?;
Ok(Some(User(user)))
}
pub async fn last_active_ip(&self) -> Option<String> {
self.0.last_active_ip.map(|ip| ip.to_string())
}
pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
self.0.last_active_at
}
}
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum OAuth2ApplicationType {
Web,
Native,
}
#[derive(Description)]
pub struct OAuth2Client(pub mas_data_model::Client);
#[Object(use_type_description)]
impl OAuth2Client {
pub async fn id(&self) -> ID {
NodeType::OAuth2Client.id(self.0.id)
}
pub async fn client_id(&self) -> &str {
&self.0.client_id
}
pub async fn client_name(&self) -> Option<&str> {
self.0.client_name.as_deref()
}
pub async fn client_uri(&self) -> Option<&Url> {
self.0.client_uri.as_ref()
}
pub async fn logo_uri(&self) -> Option<&Url> {
self.0.logo_uri.as_ref()
}
pub async fn tos_uri(&self) -> Option<&Url> {
self.0.tos_uri.as_ref()
}
pub async fn policy_uri(&self) -> Option<&Url> {
self.0.policy_uri.as_ref()
}
pub async fn redirect_uris(&self) -> &[Url] {
&self.0.redirect_uris
}
pub async fn contacts(&self) -> &[String] {
&self.0.contacts
}
pub async fn application_type(&self) -> Option<OAuth2ApplicationType> {
match self.0.application_type.as_ref()? {
ApplicationType::Web => Some(OAuth2ApplicationType::Web),
ApplicationType::Native => Some(OAuth2ApplicationType::Native),
ApplicationType::Unknown(_) => None,
}
}
}
#[derive(Description)]
pub struct OAuth2Consent {
scope: Scope,
client_id: Ulid,
}
#[Object(use_type_description)]
impl OAuth2Consent {
pub async fn scope(&self) -> String {
self.scope.to_string()
}
pub async fn client(&self, ctx: &Context<'_>) -> Result<OAuth2Client, async_graphql::Error> {
let state = ctx.state();
let mut repo = state.repository().await?;
let client = repo
.oauth2_client()
.lookup(self.client_id)
.await?
.context("Could not load client")?;
repo.cancel().await?;
Ok(OAuth2Client(client))
}
}