diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 9234e45b..a8d51b8a 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -208,6 +208,7 @@ impl Options { homeserver_connection.clone(), site_config.clone(), password_manager.clone(), + url_builder.clone(), ); let state = { diff --git a/crates/handlers/src/graphql/mod.rs b/crates/handlers/src/graphql/mod.rs index 56de6c69..73cefb15 100644 --- a/crates/handlers/src/graphql/mod.rs +++ b/crates/handlers/src/graphql/mod.rs @@ -32,6 +32,7 @@ use mas_axum_utils::{ use mas_data_model::{BrowserSession, Session, SiteConfig, User}; use mas_matrix::HomeserverConnection; use mas_policy::{InstantiateError, Policy, PolicyFactory}; +use mas_router::UrlBuilder; use mas_storage::{BoxClock, BoxRepository, BoxRng, Clock, RepositoryError, SystemClock}; use mas_storage_pg::PgRepository; use opentelemetry_semantic_conventions::trace::{GRAPHQL_DOCUMENT, GRAPHQL_OPERATION_NAME}; @@ -70,6 +71,7 @@ struct GraphQLState { policy_factory: Arc, site_config: SiteConfig, password_manager: PasswordManager, + url_builder: UrlBuilder, } #[async_trait] @@ -98,6 +100,10 @@ impl state::State for GraphQLState { self.homeserver_connection.as_ref() } + fn url_builder(&self) -> &UrlBuilder { + &self.url_builder + } + fn clock(&self) -> BoxClock { let clock = SystemClock::default(); Box::new(clock) @@ -119,6 +125,7 @@ pub fn schema( homeserver_connection: impl HomeserverConnection + 'static, site_config: SiteConfig, password_manager: PasswordManager, + url_builder: UrlBuilder, ) -> Schema { let state = GraphQLState { pool: pool.clone(), @@ -126,6 +133,7 @@ pub fn schema( homeserver_connection: Arc::new(homeserver_connection), site_config, password_manager, + url_builder, }; let state: BoxState = Box::new(state); diff --git a/crates/handlers/src/graphql/model/upstream_oauth.rs b/crates/handlers/src/graphql/model/upstream_oauth.rs index 82f451a4..f0a20648 100644 --- a/crates/handlers/src/graphql/model/upstream_oauth.rs +++ b/crates/handlers/src/graphql/model/upstream_oauth.rs @@ -8,6 +8,7 @@ use anyhow::Context as _; use async_graphql::{Context, Object, ID}; use chrono::{DateTime, Utc}; use mas_storage::{upstream_oauth2::UpstreamOAuthProviderRepository, user::UserRepository}; +use url::Url; use super::{NodeType, User}; use crate::graphql::state::ContextExt; @@ -45,6 +46,26 @@ impl UpstreamOAuth2Provider { pub async fn client_id(&self) -> &str { &self.provider.client_id } + + /// A human-readable name for this provider. + pub async fn human_name(&self) -> Option<&str> { + self.provider.human_name.as_deref() + } + + /// A brand identifier for this provider. + /// + /// One of `google`, `github`, `gitlab`, `apple` or `facebook`. + pub async fn brand_name(&self) -> Option<&str> { + self.provider.brand_name.as_deref() + } + + /// URL to start the linking process of the current user with this provider. + pub async fn link_url(&self, context: &Context<'_>) -> Url { + let state = context.state(); + let url_builder = state.url_builder(); + let route = mas_router::UpstreamOAuth2Authorize::new(self.provider.id); + url_builder.absolute_url_for(&route) + } } impl UpstreamOAuth2Link { @@ -82,6 +103,11 @@ impl UpstreamOAuth2Link { &self.link.subject } + /// A human-readable name for the link subject. + pub async fn human_account_name(&self) -> Option<&str> { + self.link.human_account_name.as_deref() + } + /// The provider for which this link is. pub async fn provider( &self, diff --git a/crates/handlers/src/graphql/state.rs b/crates/handlers/src/graphql/state.rs index fefb3f0e..f5d01ea4 100644 --- a/crates/handlers/src/graphql/state.rs +++ b/crates/handlers/src/graphql/state.rs @@ -7,6 +7,7 @@ use mas_data_model::SiteConfig; use mas_matrix::HomeserverConnection; use mas_policy::Policy; +use mas_router::UrlBuilder; use mas_storage::{BoxClock, BoxRepository, BoxRng, RepositoryError}; use crate::{graphql::Requester, passwords::PasswordManager}; @@ -20,6 +21,7 @@ pub trait State { fn clock(&self) -> BoxClock; fn rng(&self) -> BoxRng; fn site_config(&self) -> &SiteConfig; + fn url_builder(&self) -> &UrlBuilder; } pub type BoxState = Box; diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index f6a16037..a2752cf5 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -208,6 +208,7 @@ impl TestState { rng: Arc::clone(&rng), clock: Arc::clone(&clock), password_manager: password_manager.clone(), + url_builder: url_builder.clone(), }; let state: crate::graphql::BoxState = Box::new(graphql_state); @@ -370,6 +371,7 @@ struct TestGraphQLState { clock: Arc, rng: Arc>, password_manager: PasswordManager, + url_builder: UrlBuilder, } #[async_trait] @@ -394,6 +396,10 @@ impl graphql::State for TestGraphQLState { &self.homeserver_connection } + fn url_builder(&self) -> &UrlBuilder { + &self.url_builder + } + fn clock(&self) -> BoxClock { Box::new(self.clock.clone()) } diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 2ca38e29..568d3f24 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -1535,6 +1535,10 @@ type UpstreamOAuth2Link implements Node & CreationEvent { """ subject: String! """ + A human-readable name for the link subject. + """ + humanAccountName: String + """ The provider for which this link is. """ provider: UpstreamOAuth2Provider! @@ -1594,6 +1598,20 @@ type UpstreamOAuth2Provider implements Node & CreationEvent { Client ID used for this provider. """ clientId: String! + """ + A human-readable name for this provider. + """ + humanName: String + """ + A brand identifier for this provider. + + One of `google`, `github`, `gitlab`, `apple` or `facebook`. + """ + brandName: String + """ + URL to start the linking process of the current user with this provider. + """ + linkUrl: Url! } type UpstreamOAuth2ProviderConnection { diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index c6f6429a..6876b5d9 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -1116,6 +1116,8 @@ export type UpstreamOAuth2Link = CreationEvent & Node & { __typename?: 'UpstreamOAuth2Link'; /** When the object was created. */ createdAt: Scalars['DateTime']['output']; + /** A human-readable name for the link subject. */ + humanAccountName?: Maybe; /** ID of the object. */ id: Scalars['ID']['output']; /** The provider for which this link is. */ @@ -1149,14 +1151,24 @@ export type UpstreamOAuth2LinkEdge = { export type UpstreamOAuth2Provider = CreationEvent & Node & { __typename?: 'UpstreamOAuth2Provider'; + /** + * A brand identifier for this provider. + * + * One of `google`, `github`, `gitlab`, `apple` or `facebook`. + */ + brandName?: Maybe; /** Client ID used for this provider. */ clientId: Scalars['String']['output']; /** When the object was created. */ createdAt: Scalars['DateTime']['output']; + /** A human-readable name for this provider. */ + humanName?: Maybe; /** ID of the object. */ id: Scalars['ID']['output']; /** OpenID Connect issuer URL. */ issuer?: Maybe; + /** URL to start the linking process of the current user with this provider. */ + linkUrl: Scalars['Url']['output']; }; export type UpstreamOAuth2ProviderConnection = {