feat: dynamic load oauth login options if supported by gotrue

This commit is contained in:
Fu Zi Xiang 2023-11-22 22:45:15 +08:00
parent 47e49dc2dd
commit 39b5ca28ad
No known key found for this signature in database
14 changed files with 122 additions and 103 deletions

View File

@ -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`

View File

@ -1,2 +1,3 @@
# Discord
- Assets are derived from: https://discord.com/branding

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -1,2 +1,3 @@
# Github
- Assets derived from: https://github.com/logos

View File

@ -1,2 +1,3 @@
# Google OAuth Sign Logo
Assets in this directory are generated from: https://developers.google.com/identity/branding-guidelines

View File

@ -7,7 +7,9 @@ pub struct ChangePassword;
#[derive(Template)]
#[template(path = "pages/login.html")]
pub struct Login;
pub struct Login<'a> {
pub oauth_providers: Vec<&'a str>,
}
// #[derive(Template)]
// #[template(path = "login.html")]

View File

@ -64,8 +64,10 @@ pub async fn user_user_handler(
render_template(templates::UserDetails { user: &user })
}
pub async fn login_handler() -> Result<Html<String>, WebAppError> {
render_template(templates::Login {})
pub async fn login_handler(State(state): State<AppState>) -> Result<Html<String>, 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<Html<String>, WebAppError> {

View File

@ -16,9 +16,7 @@
<tr>
<td></td>
<td style="text-align: right">
<button class="button cyan" type="submit">
Invite
</button>
<button class="button cyan" type="submit">Invite</button>
</td>
</tr>
</table>

View File

@ -24,15 +24,15 @@
</html>
<script>
document.body.addEventListener('htmx:beforeRequest', function(event) {
const closeButton = event.target.querySelector('.button');
closeButton.classList.add('loading-button');
document.body.addEventListener("htmx:beforeRequest", function (event) {
const closeButton = event.target.querySelector(".button");
closeButton.classList.add("loading-button");
closeButton.disabled = true;
});
document.body.addEventListener("htmx:afterRequest", function (evt) {
const closeButton = event.target.querySelector('.button');
closeButton.classList.remove('loading-button');
const closeButton = event.target.querySelector(".button");
closeButton.classList.remove("loading-button");
closeButton.disabled = false;
const detail = evt.detail;

View File

@ -58,8 +58,10 @@
Sign In / Sign Up
</button>
</form>
<br />
<!-- Load OAuth Providers if configured -->
{% if oauth_providers.len() > 0 %}
<br />
<table style="width: 100%">
<tr style="display: flex">
<td style="width: 100%; margin: auto"><hr /></td>
@ -70,42 +72,40 @@
<h3>OAuth Login</h3>
<div id="oauth-container">
{% for provider in oauth_providers %}
<div class="oauth-icon">
<a href="/gotrue/authorize?provider=google&redirect_to=/web/login">
{% include "../assets/google/logo.html" %}
</a>
</div>
<div class="oauth-icon">
<a href="/gotrue/authorize?provider=discord&redirect_to=/web/login">
{% include "../assets/discord/logo.html" %}
</a>
</div>
<div class="oauth-icon">
<a href="/gotrue/authorize?provider=github&redirect_to=/web/login">
{% include "../assets/github/logo.html" %}
<a
href="/gotrue/authorize?provider={{ provider }}&redirect_to=/web/login"
>
<div
hx-get="../assets/{{ provider }}/logo.html"
hx-trigger="load"
hx-swap="outerHTML"
></div>
</a>
</div>
{% endfor %} {% endif %}
</div>
</div>
</div>
<script>
// OAuthLogin
if (window.location.hash) {
// Extract data from the URL fragment
const fragmentData = window.location.hash.substring(1); // Remove the leading #
const fragmentParams = new URLSearchParams(fragmentData); // Parse the fragment data as a URLSearchParams object
const refreshToken = fragmentParams.get("refresh_token"); // Extract the refresh_token
fetch(`/web-api/login_refresh/${refreshToken}`, {
// Login in via refresh_token
method: "POST",
}).then((response) => {
if (!response.ok) {
displayHttpStatusAndPayload(response);
} else {
window.location.href = "/web/home";
}
});
}
</script>
{% endblock %}
<script>
// OAuthLogin
if (window.location.hash) {
// Extract data from the URL fragment
const fragmentData = window.location.hash.substring(1); // Remove the leading #
const fragmentParams = new URLSearchParams(fragmentData); // Parse the fragment data as a URLSearchParams object
const refreshToken = fragmentParams.get("refresh_token"); // Extract the refresh_token
fetch(`/web-api/login_refresh/${refreshToken}`, {
// Login in via refresh_token
method: "POST",
}).then((response) => {
if (!response.ok) {
displayHttpStatusAndPayload(response);
} else {
window.location.href = "/web/home";
}
});
}
</script>
{% endblock %}
</div>

View File

@ -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<String, AppResponseError> {
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

View File

@ -103,16 +103,30 @@ pub struct GoTrueSettings {
pub struct GoTrueOAuthProviderSettings(BTreeMap<String, bool>);
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<A: AsRef<str>>(value: A) -> Option<OAuthProvider> {
impl AuthProvider {
pub fn from<A: AsRef<str>>(value: A) -> Option<AuthProvider> {
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,
}
}

View File

@ -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())
}

View File

@ -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());