feat: dynamic load oauth login options if supported by gotrue
This commit is contained in:
parent
47e49dc2dd
commit
39b5ca28ad
|
|
@ -1,6 +1,7 @@
|
||||||
# Admin Frontend
|
# Admin Frontend
|
||||||
|
|
||||||
## Partial Local Environment
|
## Partial Local Environment
|
||||||
|
|
||||||
- Go to source root folder of `AppFlowy-Cloud`
|
- Go to source root folder of `AppFlowy-Cloud`
|
||||||
- Start running locally dependency servers: `docker compose --file docker-compose-dev.yml up -d`
|
- 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`
|
- 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.
|
- Run `cargo watch -x run -w .`, this watch for source changes, rebuild and rerun the app.
|
||||||
|
|
||||||
## Full Local Integration Environment
|
## Full Local Integration Environment
|
||||||
|
|
||||||
- Start the whole stack: `docker compose up -d`
|
- Start the whole stack: `docker compose up -d`
|
||||||
- Go to [web server](localhost)
|
- Go to [web server](localhost)
|
||||||
- After editing source files, do `docker compose up -d --no-deps --build admin_frontend`
|
- After editing source files, do `docker compose up -d --no-deps --build admin_frontend`
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
# Discord
|
# Discord
|
||||||
|
|
||||||
- Assets are derived from: https://discord.com/branding
|
- Assets are derived from: https://discord.com/branding
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
|
|
@ -1,2 +1,3 @@
|
||||||
# Github
|
# Github
|
||||||
|
|
||||||
- Assets derived from: https://github.com/logos
|
- Assets derived from: https://github.com/logos
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
# Google OAuth Sign Logo
|
# Google OAuth Sign Logo
|
||||||
|
|
||||||
Assets in this directory are generated from: https://developers.google.com/identity/branding-guidelines
|
Assets in this directory are generated from: https://developers.google.com/identity/branding-guidelines
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ pub struct ChangePassword;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "pages/login.html")]
|
#[template(path = "pages/login.html")]
|
||||||
pub struct Login;
|
pub struct Login<'a> {
|
||||||
|
pub oauth_providers: Vec<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
// #[derive(Template)]
|
// #[derive(Template)]
|
||||||
// #[template(path = "login.html")]
|
// #[template(path = "login.html")]
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,10 @@ pub async fn user_user_handler(
|
||||||
render_template(templates::UserDetails { user: &user })
|
render_template(templates::UserDetails { user: &user })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login_handler() -> Result<Html<String>, WebAppError> {
|
pub async fn login_handler(State(state): State<AppState>) -> Result<Html<String>, WebAppError> {
|
||||||
render_template(templates::Login {})
|
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> {
|
pub async fn user_change_password_handler() -> Result<Html<String>, WebAppError> {
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
<button class="button cyan" type="submit">
|
<button class="button cyan" type="submit">Invite</button>
|
||||||
Invite
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,15 @@
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.body.addEventListener('htmx:beforeRequest', function(event) {
|
document.body.addEventListener("htmx:beforeRequest", function (event) {
|
||||||
const closeButton = event.target.querySelector('.button');
|
const closeButton = event.target.querySelector(".button");
|
||||||
closeButton.classList.add('loading-button');
|
closeButton.classList.add("loading-button");
|
||||||
closeButton.disabled = true;
|
closeButton.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.addEventListener("htmx:afterRequest", function (evt) {
|
document.body.addEventListener("htmx:afterRequest", function (evt) {
|
||||||
const closeButton = event.target.querySelector('.button');
|
const closeButton = event.target.querySelector(".button");
|
||||||
closeButton.classList.remove('loading-button');
|
closeButton.classList.remove("loading-button");
|
||||||
closeButton.disabled = false;
|
closeButton.disabled = false;
|
||||||
|
|
||||||
const detail = evt.detail;
|
const detail = evt.detail;
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,10 @@
|
||||||
Sign In / Sign Up
|
Sign In / Sign Up
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<br />
|
|
||||||
|
|
||||||
|
<!-- Load OAuth Providers if configured -->
|
||||||
|
{% if oauth_providers.len() > 0 %}
|
||||||
|
<br />
|
||||||
<table style="width: 100%">
|
<table style="width: 100%">
|
||||||
<tr style="display: flex">
|
<tr style="display: flex">
|
||||||
<td style="width: 100%; margin: auto"><hr /></td>
|
<td style="width: 100%; margin: auto"><hr /></td>
|
||||||
|
|
@ -70,42 +72,40 @@
|
||||||
|
|
||||||
<h3>OAuth Login</h3>
|
<h3>OAuth Login</h3>
|
||||||
<div id="oauth-container">
|
<div id="oauth-container">
|
||||||
|
{% for provider in oauth_providers %}
|
||||||
<div class="oauth-icon">
|
<div class="oauth-icon">
|
||||||
<a href="/gotrue/authorize?provider=google&redirect_to=/web/login">
|
<a
|
||||||
{% include "../assets/google/logo.html" %}
|
href="/gotrue/authorize?provider={{ provider }}&redirect_to=/web/login"
|
||||||
</a>
|
>
|
||||||
</div>
|
<div
|
||||||
<div class="oauth-icon">
|
hx-get="../assets/{{ provider }}/logo.html"
|
||||||
<a href="/gotrue/authorize?provider=discord&redirect_to=/web/login">
|
hx-trigger="load"
|
||||||
{% include "../assets/discord/logo.html" %}
|
hx-swap="outerHTML"
|
||||||
</a>
|
></div>
|
||||||
</div>
|
|
||||||
<div class="oauth-icon">
|
|
||||||
<a href="/gotrue/authorize?provider=github&redirect_to=/web/login">
|
|
||||||
{% include "../assets/github/logo.html" %}
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %} {% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// OAuthLogin
|
// OAuthLogin
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
// Extract data from the URL fragment
|
// Extract data from the URL fragment
|
||||||
const fragmentData = window.location.hash.substring(1); // Remove the leading #
|
const fragmentData = window.location.hash.substring(1); // Remove the leading #
|
||||||
const fragmentParams = new URLSearchParams(fragmentData); // Parse the fragment data as a URLSearchParams object
|
const fragmentParams = new URLSearchParams(fragmentData); // Parse the fragment data as a URLSearchParams object
|
||||||
const refreshToken = fragmentParams.get("refresh_token"); // Extract the refresh_token
|
const refreshToken = fragmentParams.get("refresh_token"); // Extract the refresh_token
|
||||||
fetch(`/web-api/login_refresh/${refreshToken}`, {
|
fetch(`/web-api/login_refresh/${refreshToken}`, {
|
||||||
// Login in via refresh_token
|
// Login in via refresh_token
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
displayHttpStatusAndPayload(response);
|
displayHttpStatusAndPayload(response);
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/web/home";
|
window.location.href = "/web/home";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ use url::Url;
|
||||||
use crate::retry::{RefreshTokenAction, RefreshTokenRetryCondition};
|
use crate::retry::{RefreshTokenAction, RefreshTokenRetryCondition};
|
||||||
use crate::ws::{WSClientHttpSender, WSError};
|
use crate::ws::{WSClientHttpSender, WSError};
|
||||||
use gotrue_entity::dto::SignUpResponse::{Authenticated, NotAuthenticated};
|
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;
|
use realtime_entity::realtime_proto::HttpRealtimeMessage;
|
||||||
|
|
||||||
/// `Client` is responsible for managing communication with the GoTrue API and cloud storage.
|
/// `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)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn generate_oauth_url_with_provider(
|
pub async fn generate_oauth_url_with_provider(
|
||||||
&self,
|
&self,
|
||||||
provider: &OAuthProvider,
|
provider: &AuthProvider,
|
||||||
) -> Result<String, AppResponseError> {
|
) -> Result<String, AppResponseError> {
|
||||||
let settings = self.gotrue_client.settings().await?;
|
let settings = self.gotrue_client.settings().await?;
|
||||||
if !settings.external.has_provider(provider) {
|
if !settings.external.has_provider(provider) {
|
||||||
|
|
@ -225,7 +225,7 @@ impl Client {
|
||||||
.append_pair("provider", provider.as_str())
|
.append_pair("provider", provider.as_str())
|
||||||
.append_pair("redirect_to", DESKTOP_CALLBACK_URL);
|
.append_pair("redirect_to", DESKTOP_CALLBACK_URL);
|
||||||
|
|
||||||
if let OAuthProvider::Google = provider {
|
if let AuthProvider::Google = provider {
|
||||||
url
|
url
|
||||||
.query_pairs_mut()
|
.query_pairs_mut()
|
||||||
// In many cases, especially for server-side applications or mobile apps that might need to
|
// In many cases, especially for server-side applications or mobile apps that might need to
|
||||||
|
|
|
||||||
|
|
@ -103,16 +103,30 @@ pub struct GoTrueSettings {
|
||||||
pub struct GoTrueOAuthProviderSettings(BTreeMap<String, bool>);
|
pub struct GoTrueOAuthProviderSettings(BTreeMap<String, bool>);
|
||||||
|
|
||||||
impl GoTrueOAuthProviderSettings {
|
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());
|
let a = self.0.get(p.as_str());
|
||||||
match a {
|
match a {
|
||||||
Some(v) => *v,
|
Some(v) => *v,
|
||||||
None => false,
|
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,
|
Apple,
|
||||||
Azure,
|
Azure,
|
||||||
Bitbucket,
|
Bitbucket,
|
||||||
|
|
@ -131,63 +145,61 @@ pub enum OAuthProvider {
|
||||||
Workos,
|
Workos,
|
||||||
Twitch,
|
Twitch,
|
||||||
Twitter,
|
Twitter,
|
||||||
Email,
|
|
||||||
Phone,
|
|
||||||
Zoom,
|
Zoom,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OAuthProvider {
|
impl AuthProvider {
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
OAuthProvider::Apple => "apple",
|
AuthProvider::Apple => "apple",
|
||||||
OAuthProvider::Azure => "azure",
|
AuthProvider::Azure => "azure",
|
||||||
OAuthProvider::Bitbucket => "bitbucket",
|
AuthProvider::Bitbucket => "bitbucket",
|
||||||
OAuthProvider::Discord => "discord",
|
AuthProvider::Discord => "discord",
|
||||||
OAuthProvider::Facebook => "facebook",
|
AuthProvider::Facebook => "facebook",
|
||||||
OAuthProvider::Figma => "figma",
|
AuthProvider::Figma => "figma",
|
||||||
OAuthProvider::Github => "github",
|
AuthProvider::Github => "github",
|
||||||
OAuthProvider::Gitlab => "gitlab",
|
AuthProvider::Gitlab => "gitlab",
|
||||||
OAuthProvider::Google => "google",
|
AuthProvider::Google => "google",
|
||||||
OAuthProvider::Keycloak => "keycloak",
|
AuthProvider::Keycloak => "keycloak",
|
||||||
OAuthProvider::Kakao => "kakao",
|
AuthProvider::Kakao => "kakao",
|
||||||
OAuthProvider::Linkedin => "linkedin",
|
AuthProvider::Linkedin => "linkedin",
|
||||||
OAuthProvider::Notion => "notion",
|
AuthProvider::Notion => "notion",
|
||||||
OAuthProvider::Spotify => "spotify",
|
AuthProvider::Spotify => "spotify",
|
||||||
OAuthProvider::Slack => "slack",
|
AuthProvider::Slack => "slack",
|
||||||
OAuthProvider::Workos => "workos",
|
AuthProvider::Workos => "workos",
|
||||||
OAuthProvider::Twitch => "twitch",
|
AuthProvider::Twitch => "twitch",
|
||||||
OAuthProvider::Twitter => "twitter",
|
AuthProvider::Twitter => "twitter",
|
||||||
OAuthProvider::Email => "email",
|
AuthProvider::Email => "email",
|
||||||
OAuthProvider::Phone => "phone",
|
AuthProvider::Phone => "phone",
|
||||||
OAuthProvider::Zoom => "zoom",
|
AuthProvider::Zoom => "zoom",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OAuthProvider {
|
impl AuthProvider {
|
||||||
pub fn from<A: AsRef<str>>(value: A) -> Option<OAuthProvider> {
|
pub fn from<A: AsRef<str>>(value: A) -> Option<AuthProvider> {
|
||||||
match value.as_ref() {
|
match value.as_ref() {
|
||||||
"apple" => Some(OAuthProvider::Apple),
|
"apple" => Some(AuthProvider::Apple),
|
||||||
"azure" => Some(OAuthProvider::Azure),
|
"azure" => Some(AuthProvider::Azure),
|
||||||
"bitbucket" => Some(OAuthProvider::Bitbucket),
|
"bitbucket" => Some(AuthProvider::Bitbucket),
|
||||||
"discord" => Some(OAuthProvider::Discord),
|
"discord" => Some(AuthProvider::Discord),
|
||||||
"facebook" => Some(OAuthProvider::Facebook),
|
"facebook" => Some(AuthProvider::Facebook),
|
||||||
"figma" => Some(OAuthProvider::Figma),
|
"figma" => Some(AuthProvider::Figma),
|
||||||
"github" => Some(OAuthProvider::Github),
|
"github" => Some(AuthProvider::Github),
|
||||||
"gitlab" => Some(OAuthProvider::Gitlab),
|
"gitlab" => Some(AuthProvider::Gitlab),
|
||||||
"google" => Some(OAuthProvider::Google),
|
"google" => Some(AuthProvider::Google),
|
||||||
"keycloak" => Some(OAuthProvider::Keycloak),
|
"keycloak" => Some(AuthProvider::Keycloak),
|
||||||
"kakao" => Some(OAuthProvider::Kakao),
|
"kakao" => Some(AuthProvider::Kakao),
|
||||||
"linkedin" => Some(OAuthProvider::Linkedin),
|
"linkedin" => Some(AuthProvider::Linkedin),
|
||||||
"notion" => Some(OAuthProvider::Notion),
|
"notion" => Some(AuthProvider::Notion),
|
||||||
"spotify" => Some(OAuthProvider::Spotify),
|
"spotify" => Some(AuthProvider::Spotify),
|
||||||
"slack" => Some(OAuthProvider::Slack),
|
"slack" => Some(AuthProvider::Slack),
|
||||||
"workos" => Some(OAuthProvider::Workos),
|
"workos" => Some(AuthProvider::Workos),
|
||||||
"twitch" => Some(OAuthProvider::Twitch),
|
"twitch" => Some(AuthProvider::Twitch),
|
||||||
"twitter" => Some(OAuthProvider::Twitter),
|
"twitter" => Some(AuthProvider::Twitter),
|
||||||
"email" => Some(OAuthProvider::Email),
|
"email" => Some(AuthProvider::Email),
|
||||||
"phone" => Some(OAuthProvider::Phone),
|
"phone" => Some(AuthProvider::Phone),
|
||||||
"zoom" => Some(OAuthProvider::Zoom),
|
"zoom" => Some(AuthProvider::Zoom),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use crate::params::{
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use gotrue_entity::dto::{
|
use gotrue_entity::dto::{
|
||||||
AdminListUsersResponse, GoTrueSettings, GotrueTokenResponse, OAuthProvider, SignUpResponse,
|
AdminListUsersResponse, AuthProvider, GoTrueSettings, GotrueTokenResponse, SignUpResponse,
|
||||||
UpdateGotrueUserParams, User,
|
UpdateGotrueUserParams, User,
|
||||||
};
|
};
|
||||||
use gotrue_entity::error::{GoTrueError, GoTrueErrorSerde, GotrueClientError};
|
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())
|
format!("{}/authorize?provider={}", self.base_url, provider.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use app_error::ErrorCode;
|
use app_error::ErrorCode;
|
||||||
use gotrue_entity::dto::OAuthProvider;
|
use gotrue_entity::dto::AuthProvider;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
localhost_client, test_appflowy_cloud_client,
|
localhost_client, test_appflowy_cloud_client,
|
||||||
|
|
@ -52,7 +52,7 @@ async fn sign_up_but_existing_user() {
|
||||||
async fn sign_up_oauth_not_available() {
|
async fn sign_up_oauth_not_available() {
|
||||||
let c = localhost_client();
|
let c = localhost_client();
|
||||||
let err = c
|
let err = c
|
||||||
.generate_oauth_url_with_provider(&OAuthProvider::Zoom)
|
.generate_oauth_url_with_provider(&AuthProvider::Zoom)
|
||||||
.await
|
.await
|
||||||
.err()
|
.err()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -68,14 +68,14 @@ async fn sign_up_oauth_not_available() {
|
||||||
async fn sign_up_with_google_oauth() {
|
async fn sign_up_with_google_oauth() {
|
||||||
let c = localhost_client();
|
let c = localhost_client();
|
||||||
let url = c
|
let url = c
|
||||||
.generate_oauth_url_with_provider(&OAuthProvider::Google)
|
.generate_oauth_url_with_provider(&AuthProvider::Google)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!url.is_empty());
|
assert!(!url.is_empty());
|
||||||
|
|
||||||
let c = test_appflowy_cloud_client();
|
let c = test_appflowy_cloud_client();
|
||||||
let url = c
|
let url = c
|
||||||
.generate_oauth_url_with_provider(&OAuthProvider::Google)
|
.generate_oauth_url_with_provider(&AuthProvider::Google)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!url.is_empty());
|
assert!(!url.is_empty());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue