mas_handlers/graphql/model/
upstream_oauth.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 anyhow::Context as _;
8use async_graphql::{Context, ID, Object};
9use chrono::{DateTime, Utc};
10use mas_storage::{upstream_oauth2::UpstreamOAuthProviderRepository, user::UserRepository};
11use url::Url;
12
13use super::{NodeType, User};
14use crate::graphql::state::ContextExt;
15
16#[derive(Debug, Clone)]
17pub struct UpstreamOAuth2Provider {
18    provider: mas_data_model::UpstreamOAuthProvider,
19}
20
21impl UpstreamOAuth2Provider {
22    #[must_use]
23    pub const fn new(provider: mas_data_model::UpstreamOAuthProvider) -> Self {
24        Self { provider }
25    }
26}
27
28#[Object]
29impl UpstreamOAuth2Provider {
30    /// ID of the object.
31    pub async fn id(&self) -> ID {
32        NodeType::UpstreamOAuth2Provider.id(self.provider.id)
33    }
34
35    /// When the object was created.
36    pub async fn created_at(&self) -> DateTime<Utc> {
37        self.provider.created_at
38    }
39
40    /// OpenID Connect issuer URL.
41    pub async fn issuer(&self) -> Option<&str> {
42        self.provider.issuer.as_deref()
43    }
44
45    /// Client ID used for this provider.
46    pub async fn client_id(&self) -> &str {
47        &self.provider.client_id
48    }
49
50    /// A human-readable name for this provider.
51    pub async fn human_name(&self) -> Option<&str> {
52        self.provider.human_name.as_deref()
53    }
54
55    /// A brand identifier for this provider.
56    ///
57    /// One of `google`, `github`, `gitlab`, `apple` or `facebook`.
58    pub async fn brand_name(&self) -> Option<&str> {
59        self.provider.brand_name.as_deref()
60    }
61
62    /// URL to start the linking process of the current user with this provider.
63    pub async fn link_url(&self, context: &Context<'_>) -> Url {
64        let state = context.state();
65        let url_builder = state.url_builder();
66        let route = mas_router::UpstreamOAuth2Authorize::new(self.provider.id);
67        url_builder.absolute_url_for(&route)
68    }
69}
70
71impl UpstreamOAuth2Link {
72    #[must_use]
73    pub const fn new(link: mas_data_model::UpstreamOAuthLink) -> Self {
74        Self {
75            link,
76            provider: None,
77            user: None,
78        }
79    }
80}
81
82#[derive(Debug, Clone)]
83pub struct UpstreamOAuth2Link {
84    link: mas_data_model::UpstreamOAuthLink,
85    provider: Option<mas_data_model::UpstreamOAuthProvider>,
86    user: Option<mas_data_model::User>,
87}
88
89#[Object]
90impl UpstreamOAuth2Link {
91    /// ID of the object.
92    pub async fn id(&self) -> ID {
93        NodeType::UpstreamOAuth2Link.id(self.link.id)
94    }
95
96    /// When the object was created.
97    pub async fn created_at(&self) -> DateTime<Utc> {
98        self.link.created_at
99    }
100
101    /// Subject used for linking
102    pub async fn subject(&self) -> &str {
103        &self.link.subject
104    }
105
106    /// A human-readable name for the link subject.
107    pub async fn human_account_name(&self) -> Option<&str> {
108        self.link.human_account_name.as_deref()
109    }
110
111    /// The provider for which this link is.
112    pub async fn provider(
113        &self,
114        ctx: &Context<'_>,
115    ) -> Result<UpstreamOAuth2Provider, async_graphql::Error> {
116        let state = ctx.state();
117        let provider = if let Some(provider) = &self.provider {
118            // Cached
119            provider.clone()
120        } else {
121            // Fetch on-the-fly
122            let mut repo = state.repository().await?;
123
124            let provider = repo
125                .upstream_oauth_provider()
126                .lookup(self.link.provider_id)
127                .await?
128                .context("Upstream OAuth 2.0 provider not found")?;
129            repo.cancel().await?;
130
131            provider
132        };
133
134        Ok(UpstreamOAuth2Provider::new(provider))
135    }
136
137    /// The user to which this link is associated.
138    pub async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
139        let state = ctx.state();
140        let user = if let Some(user) = &self.user {
141            // Cached
142            user.clone()
143        } else if let Some(user_id) = &self.link.user_id {
144            // Fetch on-the-fly
145            let mut repo = state.repository().await?;
146
147            let user = repo
148                .user()
149                .lookup(*user_id)
150                .await?
151                .context("User not found")?;
152            repo.cancel().await?;
153
154            user
155        } else {
156            return Ok(None);
157        };
158
159        Ok(Some(User(user)))
160    }
161}