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
|
||||
|
||||
## 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`
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
# Discord
|
||||
|
||||
- Assets are derived from: https://discord.com/branding
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
|
|
@ -1,2 +1,3 @@
|
|||
# Github
|
||||
|
||||
- Assets derived from: https://github.com/logos
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
# Google OAuth Sign Logo
|
||||
|
||||
Assets in this directory are generated from: https://developers.google.com/identity/branding-guidelines
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Reference in New Issue