From 39b5ca28ad624b715321088d32de7a3578aa5a1d Mon Sep 17 00:00:00 2001 From: Fu Zi Xiang Date: Wed, 22 Nov 2023 22:45:15 +0800 Subject: [PATCH] feat: dynamic load oauth login options if supported by gotrue --- admin_frontend/README.md | 2 + admin_frontend/assets/discord/README.md | 1 + admin_frontend/assets/esim_turkey.png | Bin 0 -> 5594 bytes admin_frontend/assets/github/README.md | 1 + admin_frontend/assets/google/README.md | 1 + admin_frontend/src/templates.rs | 4 +- admin_frontend/src/web_app.rs | 6 +- .../templates/components/invite.html | 4 +- admin_frontend/templates/layouts/base.html | 10 +- admin_frontend/templates/pages/login.html | 68 +++++------ libs/client-api/src/http.rs | 6 +- libs/gotrue-entity/src/dto.rs | 110 ++++++++++-------- libs/gotrue/src/api.rs | 4 +- tests/user/sign_up.rs | 8 +- 14 files changed, 122 insertions(+), 103 deletions(-) create mode 100644 admin_frontend/assets/esim_turkey.png diff --git a/admin_frontend/README.md b/admin_frontend/README.md index ce26934f..a2b8c75c 100644 --- a/admin_frontend/README.md +++ b/admin_frontend/README.md @@ -1,6 +1,7 @@ # Admin Frontend ## Partial Local Environment + - Go to source root folder of `AppFlowy-Cloud` - Start running locally dependency servers: `docker compose --file docker-compose-dev.yml up -d` - Start SQLX migrations `cargo sqlx database create && cargo sqlx migrate run && cargo sqlx prepare --workspace` @@ -9,6 +10,7 @@ - Run `cargo watch -x run -w .`, this watch for source changes, rebuild and rerun the app. ## Full Local Integration Environment + - Start the whole stack: `docker compose up -d` - Go to [web server](localhost) - After editing source files, do `docker compose up -d --no-deps --build admin_frontend` diff --git a/admin_frontend/assets/discord/README.md b/admin_frontend/assets/discord/README.md index e8208fc9..bea6203a 100644 --- a/admin_frontend/assets/discord/README.md +++ b/admin_frontend/assets/discord/README.md @@ -1,2 +1,3 @@ # Discord + - Assets are derived from: https://discord.com/branding diff --git a/admin_frontend/assets/esim_turkey.png b/admin_frontend/assets/esim_turkey.png new file mode 100644 index 0000000000000000000000000000000000000000..eefac59a1a85a8bb06aa71de0f802f73edc3d2d0 GIT binary patch literal 5594 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 { + pub oauth_providers: Vec<&'a str>, +} // #[derive(Template)] // #[template(path = "login.html")] diff --git a/admin_frontend/src/web_app.rs b/admin_frontend/src/web_app.rs index 6c4b819c..c41b787a 100644 --- a/admin_frontend/src/web_app.rs +++ b/admin_frontend/src/web_app.rs @@ -64,8 +64,10 @@ pub async fn user_user_handler( render_template(templates::UserDetails { user: &user }) } -pub async fn login_handler() -> Result, WebAppError> { - render_template(templates::Login {}) +pub async fn login_handler(State(state): State) -> Result, WebAppError> { + let external = state.gotrue_client.settings().await?.external; + let oauth_providers = external.oauth_providers(); + render_template(templates::Login { oauth_providers }) } pub async fn user_change_password_handler() -> Result, WebAppError> { diff --git a/admin_frontend/templates/components/invite.html b/admin_frontend/templates/components/invite.html index 34af82c2..6a41bd79 100644 --- a/admin_frontend/templates/components/invite.html +++ b/admin_frontend/templates/components/invite.html @@ -16,9 +16,7 @@ - + diff --git a/admin_frontend/templates/layouts/base.html b/admin_frontend/templates/layouts/base.html index 51d566fc..92b43e8e 100644 --- a/admin_frontend/templates/layouts/base.html +++ b/admin_frontend/templates/layouts/base.html @@ -24,15 +24,15 @@ -{% endblock %} + + {% endblock %} + diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 175bbc81..47f63b4a 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -44,7 +44,7 @@ use url::Url; use crate::retry::{RefreshTokenAction, RefreshTokenRetryCondition}; use crate::ws::{WSClientHttpSender, WSError}; use gotrue_entity::dto::SignUpResponse::{Authenticated, NotAuthenticated}; -use gotrue_entity::dto::{GotrueTokenResponse, OAuthProvider, UpdateGotrueUserParams, User}; +use gotrue_entity::dto::{GotrueTokenResponse, AuthProvider, UpdateGotrueUserParams, User}; use realtime_entity::realtime_proto::HttpRealtimeMessage; /// `Client` is responsible for managing communication with the GoTrue API and cloud storage. @@ -210,7 +210,7 @@ impl Client { #[instrument(level = "debug", skip_all, err)] pub async fn generate_oauth_url_with_provider( &self, - provider: &OAuthProvider, + provider: &AuthProvider, ) -> Result { let settings = self.gotrue_client.settings().await?; if !settings.external.has_provider(provider) { @@ -225,7 +225,7 @@ impl Client { .append_pair("provider", provider.as_str()) .append_pair("redirect_to", DESKTOP_CALLBACK_URL); - if let OAuthProvider::Google = provider { + if let AuthProvider::Google = provider { url .query_pairs_mut() // In many cases, especially for server-side applications or mobile apps that might need to diff --git a/libs/gotrue-entity/src/dto.rs b/libs/gotrue-entity/src/dto.rs index 422347bb..5bebbd74 100644 --- a/libs/gotrue-entity/src/dto.rs +++ b/libs/gotrue-entity/src/dto.rs @@ -103,16 +103,30 @@ pub struct GoTrueSettings { pub struct GoTrueOAuthProviderSettings(BTreeMap); impl GoTrueOAuthProviderSettings { - pub fn has_provider(&self, p: &OAuthProvider) -> bool { + pub fn has_provider(&self, p: &AuthProvider) -> bool { let a = self.0.get(p.as_str()); match a { Some(v) => *v, None => false, } } + + pub fn oauth_providers(&self) -> Vec<&str> { + self + .0 + .iter() + .filter(|&(key, &value)| value && key != "email" && key != "phone") + .map(|(key, _value)| key.as_str()) + .collect() + } } -pub enum OAuthProvider { +pub enum AuthProvider { + // Non-OAuth providers + Email, + Phone, + + // OAuth providers Apple, Azure, Bitbucket, @@ -131,63 +145,61 @@ pub enum OAuthProvider { Workos, Twitch, Twitter, - Email, - Phone, Zoom, } -impl OAuthProvider { +impl AuthProvider { pub fn as_str(&self) -> &str { match self { - OAuthProvider::Apple => "apple", - OAuthProvider::Azure => "azure", - OAuthProvider::Bitbucket => "bitbucket", - OAuthProvider::Discord => "discord", - OAuthProvider::Facebook => "facebook", - OAuthProvider::Figma => "figma", - OAuthProvider::Github => "github", - OAuthProvider::Gitlab => "gitlab", - OAuthProvider::Google => "google", - OAuthProvider::Keycloak => "keycloak", - OAuthProvider::Kakao => "kakao", - OAuthProvider::Linkedin => "linkedin", - OAuthProvider::Notion => "notion", - OAuthProvider::Spotify => "spotify", - OAuthProvider::Slack => "slack", - OAuthProvider::Workos => "workos", - OAuthProvider::Twitch => "twitch", - OAuthProvider::Twitter => "twitter", - OAuthProvider::Email => "email", - OAuthProvider::Phone => "phone", - OAuthProvider::Zoom => "zoom", + AuthProvider::Apple => "apple", + AuthProvider::Azure => "azure", + AuthProvider::Bitbucket => "bitbucket", + AuthProvider::Discord => "discord", + AuthProvider::Facebook => "facebook", + AuthProvider::Figma => "figma", + AuthProvider::Github => "github", + AuthProvider::Gitlab => "gitlab", + AuthProvider::Google => "google", + AuthProvider::Keycloak => "keycloak", + AuthProvider::Kakao => "kakao", + AuthProvider::Linkedin => "linkedin", + AuthProvider::Notion => "notion", + AuthProvider::Spotify => "spotify", + AuthProvider::Slack => "slack", + AuthProvider::Workos => "workos", + AuthProvider::Twitch => "twitch", + AuthProvider::Twitter => "twitter", + AuthProvider::Email => "email", + AuthProvider::Phone => "phone", + AuthProvider::Zoom => "zoom", } } } -impl OAuthProvider { - pub fn from>(value: A) -> Option { +impl AuthProvider { + pub fn from>(value: A) -> Option { match value.as_ref() { - "apple" => Some(OAuthProvider::Apple), - "azure" => Some(OAuthProvider::Azure), - "bitbucket" => Some(OAuthProvider::Bitbucket), - "discord" => Some(OAuthProvider::Discord), - "facebook" => Some(OAuthProvider::Facebook), - "figma" => Some(OAuthProvider::Figma), - "github" => Some(OAuthProvider::Github), - "gitlab" => Some(OAuthProvider::Gitlab), - "google" => Some(OAuthProvider::Google), - "keycloak" => Some(OAuthProvider::Keycloak), - "kakao" => Some(OAuthProvider::Kakao), - "linkedin" => Some(OAuthProvider::Linkedin), - "notion" => Some(OAuthProvider::Notion), - "spotify" => Some(OAuthProvider::Spotify), - "slack" => Some(OAuthProvider::Slack), - "workos" => Some(OAuthProvider::Workos), - "twitch" => Some(OAuthProvider::Twitch), - "twitter" => Some(OAuthProvider::Twitter), - "email" => Some(OAuthProvider::Email), - "phone" => Some(OAuthProvider::Phone), - "zoom" => Some(OAuthProvider::Zoom), + "apple" => Some(AuthProvider::Apple), + "azure" => Some(AuthProvider::Azure), + "bitbucket" => Some(AuthProvider::Bitbucket), + "discord" => Some(AuthProvider::Discord), + "facebook" => Some(AuthProvider::Facebook), + "figma" => Some(AuthProvider::Figma), + "github" => Some(AuthProvider::Github), + "gitlab" => Some(AuthProvider::Gitlab), + "google" => Some(AuthProvider::Google), + "keycloak" => Some(AuthProvider::Keycloak), + "kakao" => Some(AuthProvider::Kakao), + "linkedin" => Some(AuthProvider::Linkedin), + "notion" => Some(AuthProvider::Notion), + "spotify" => Some(AuthProvider::Spotify), + "slack" => Some(AuthProvider::Slack), + "workos" => Some(AuthProvider::Workos), + "twitch" => Some(AuthProvider::Twitch), + "twitter" => Some(AuthProvider::Twitter), + "email" => Some(AuthProvider::Email), + "phone" => Some(AuthProvider::Phone), + "zoom" => Some(AuthProvider::Zoom), _ => None, } } diff --git a/libs/gotrue/src/api.rs b/libs/gotrue/src/api.rs index 7671cb09..7aae8a39 100644 --- a/libs/gotrue/src/api.rs +++ b/libs/gotrue/src/api.rs @@ -4,7 +4,7 @@ use crate::params::{ }; use anyhow::Context; use gotrue_entity::dto::{ - AdminListUsersResponse, GoTrueSettings, GotrueTokenResponse, OAuthProvider, SignUpResponse, + AdminListUsersResponse, AuthProvider, GoTrueSettings, GotrueTokenResponse, SignUpResponse, UpdateGotrueUserParams, User, }; use gotrue_entity::error::{GoTrueError, GoTrueErrorSerde, GotrueClientError}; @@ -24,7 +24,7 @@ impl Client { } } - pub fn oauth_url(&self, provider: &OAuthProvider) -> String { + pub fn oauth_url(&self, provider: &AuthProvider) -> String { format!("{}/authorize?provider={}", self.base_url, provider.as_str()) } diff --git a/tests/user/sign_up.rs b/tests/user/sign_up.rs index a822db18..573c6fab 100644 --- a/tests/user/sign_up.rs +++ b/tests/user/sign_up.rs @@ -1,5 +1,5 @@ use app_error::ErrorCode; -use gotrue_entity::dto::OAuthProvider; +use gotrue_entity::dto::AuthProvider; use crate::{ localhost_client, test_appflowy_cloud_client, @@ -52,7 +52,7 @@ async fn sign_up_but_existing_user() { async fn sign_up_oauth_not_available() { let c = localhost_client(); let err = c - .generate_oauth_url_with_provider(&OAuthProvider::Zoom) + .generate_oauth_url_with_provider(&AuthProvider::Zoom) .await .err() .unwrap(); @@ -68,14 +68,14 @@ async fn sign_up_oauth_not_available() { async fn sign_up_with_google_oauth() { let c = localhost_client(); let url = c - .generate_oauth_url_with_provider(&OAuthProvider::Google) + .generate_oauth_url_with_provider(&AuthProvider::Google) .await .unwrap(); assert!(!url.is_empty()); let c = test_appflowy_cloud_client(); let url = c - .generate_oauth_url_with_provider(&OAuthProvider::Google) + .generate_oauth_url_with_provider(&AuthProvider::Google) .await .unwrap(); assert!(!url.is_empty());