Merge pull request #407 from AppFlowy-IO/admin-frontend/email-invite

Admin frontend/email invite
This commit is contained in:
Zack 2024-03-21 21:11:17 +08:00 committed by GitHub
commit 68b0769bf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 62 additions and 40 deletions

View File

@ -28,11 +28,11 @@ pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/signin", post(sign_in_handler)) .route("/signin", post(sign_in_handler))
.route("/signup", post(sign_up_handler)) .route("/signup", post(sign_up_handler))
.route("/login_refresh/:refresh_token", post(login_refresh_handler)) .route("/login-refresh/:refresh_token", post(login_refresh_handler))
.route("/logout", post(logout_handler)) .route("/logout", post(logout_handler))
// user // user
.route("/change_password", post(change_password_handler)) .route("/change-password", post(change_password_handler))
.route("/oauth_login/:provider", post(post_oauth_login_handler)) .route("/oauth_login/:provider", post(post_oauth_login_handler))
.route("/invite", post(invite_handler)) .route("/invite", post(invite_handler))
.route("/workspace/:workspace_id/invite", post(workspace_invite_handler)) .route("/workspace/:workspace_id/invite", post(workspace_invite_handler))

View File

@ -3,6 +3,7 @@
class="sidebar-item" class="sidebar-item"
hx-target="#sidebar-content" hx-target="#sidebar-content"
hx-get="/web/components/user/navigate" hx-get="/web/components/user/navigate"
data-section="navigate"
> >
Navigate Navigate
</div> </div>
@ -10,6 +11,7 @@
class="sidebar-item" class="sidebar-item"
hx-target="#sidebar-content" hx-target="#sidebar-content"
hx-get="/web/components/user/change-password" hx-get="/web/components/user/change-password"
data-section="change-password"
> >
Change Password Change Password
</div> </div>
@ -17,6 +19,7 @@
class="sidebar-item" class="sidebar-item"
hx-target="#sidebar-content" hx-target="#sidebar-content"
hx-get="/web/components/user/invite" hx-get="/web/components/user/invite"
data-section="invite"
> >
Invite Invite
</div> </div>
@ -24,6 +27,7 @@
class="sidebar-item" class="sidebar-item"
hx-target="#sidebar-content" hx-target="#sidebar-content"
hx-get="/web/components/user/user-usage" hx-get="/web/components/user/user-usage"
data-section="user-usage"
> >
User Usage User Usage
</div> </div>
@ -31,7 +35,24 @@
class="sidebar-item" class="sidebar-item"
hx-target="#sidebar-content" hx-target="#sidebar-content"
hx-get="/web/components/user/workspace-usage" hx-get="/web/components/user/workspace-usage"
data-section="workspace-usage"
> >
Workspace Usage Workspace Usage
</div> </div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const frag = window.location.href.split('#');
if (frag.length > 1) {
const section = frag[1];
const sidebarItems = document.querySelectorAll('.sidebar-item');
sidebarItems.forEach(item => {
if (item.getAttribute('data-section') === section) {
item.click();
}
});
}
});
</script>

View File

@ -107,22 +107,46 @@
</div> </div>
<script> <script>
// OAuthLogin // OAuthLogin (This is done because axum doesn't support fragment in the URL)
// https://github.com/tokio-rs/axum/discussions/2147
// Parse the fragment and extract the parameters
const paramMap = {};
if (window.location.hash) { if (window.location.hash) {
// Extract data from the URL fragment const fragments = window.location.href.split('#').slice(1);
const fragmentData = window.location.hash.substring(1); // Remove the leading # for (let i = 0; i < fragments.length; i++) {
const fragmentParams = new URLSearchParams(fragmentData); // Parse the fragment data as a URLSearchParams object const params = fragments[i].split('&');
const refreshToken = fragmentParams.get("refresh_token"); // Extract the refresh_token for (let j = 0; j < params.length; j++) {
fetch(`/web-api/login_refresh/${refreshToken}`, { const keyValue = params[j].split('=');
// Login in via refresh_token if (keyValue.length === 2) {
const key = keyValue[0];
const value = keyValue[1];
paramMap[key] = value;
}
}
}
// Clear the fragment from the URL
history.replaceState("", document.title, window.location.pathname + window.location.search);
// Login in via refresh_token
const refreshToken = paramMap['refresh_token'];
fetch(`/web-api/login-refresh/${refreshToken}`, {
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"; const redirect_to = paramMap['redirect_to'];
if (redirect_to && redirect_to.trim() !== "") {
console.log("redirect_to is not empty");
window.location.href = "/web/home#" + redirect_to;
} else {
window.location.href = "/web/home";
}
} }
}); });
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@ -250,7 +250,6 @@ async fn post_workspace_invite_handler(
let invited_members = payload.into_inner(); let invited_members = payload.into_inner();
workspace::ops::invite_workspace_members( workspace::ops::invite_workspace_members(
&state.pg_pool, &state.pg_pool,
&state.gotrue_admin,
&state.gotrue_client, &state.gotrue_client,
&user_uuid, &user_uuid,
&workspace_id, &workspace_id,

View File

@ -1,5 +1,4 @@
use crate::biz::workspace::access_control::WorkspaceAccessControl; use crate::biz::workspace::access_control::WorkspaceAccessControl;
use crate::state::GoTrueAdmin;
use anyhow::Context; use anyhow::Context;
use app_error::AppError; use app_error::AppError;
use database::collab::upsert_collab_member_with_txn; use database::collab::upsert_collab_member_with_txn;
@ -21,7 +20,7 @@ use database_entity::dto::{
WorkspaceUsage, WorkspaceUsage,
}; };
use gotrue::params::InviteUserParams; use gotrue::params::MagicLinkParams;
use shared_entity::dto::workspace_dto::{ use shared_entity::dto::workspace_dto::{
CreateWorkspaceMember, WorkspaceMemberChangeset, WorkspaceMemberInvitation, CreateWorkspaceMember, WorkspaceMemberChangeset, WorkspaceMemberInvitation,
}; };
@ -31,7 +30,7 @@ use sqlx::{types::uuid, PgPool};
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::sync::Arc; use std::sync::Arc;
use tracing::{info, instrument}; use tracing::instrument;
use uuid::Uuid; use uuid::Uuid;
use workspace_template::document::get_started::GetStartedDocumentTemplate; use workspace_template::document::get_started::GetStartedDocumentTemplate;
@ -164,7 +163,6 @@ pub async fn accept_workspace_invite(
#[instrument(level = "debug", skip_all, err)] #[instrument(level = "debug", skip_all, err)]
pub async fn invite_workspace_members( pub async fn invite_workspace_members(
pg_pool: &PgPool, pg_pool: &PgPool,
gotrue_admin: &GoTrueAdmin,
gotrue_client: &gotrue::api::Client, gotrue_client: &gotrue::api::Client,
inviter: &Uuid, inviter: &Uuid,
workspace_id: &Uuid, workspace_id: &Uuid,
@ -175,36 +173,16 @@ pub async fn invite_workspace_members(
.await .await
.context("Begin transaction to invite workspace members")?; .context("Begin transaction to invite workspace members")?;
let admin_token = gotrue_admin.token(gotrue_client).await?;
for invitation in invitations { for invitation in invitations {
match gotrue_client gotrue_client
.admin_invite_user( .magic_link(
&admin_token, &MagicLinkParams {
&InviteUserParams {
email: invitation.email.clone(), email: invitation.email.clone(),
..Default::default() ..Default::default()
}, },
Some("/web/home#redirect_to=invite".to_owned()),
) )
.await .await?;
{
Ok(new_user) => {
info!(
"Invited new user: {:?} to workspace: {:?}",
new_user, workspace_id
);
},
Err(err) => match err {
app_error::gotrue::GoTrueError::Internal(ref err_serde) => {
match (err_serde.code, err_serde.msg.as_str()) {
(422, "A user with this email address has already been registered") => {
info!("User already exists, skipping invite");
},
_ => return Err(AppError::Internal(err.into())),
}
},
_ => return Err(err.into()),
},
}
// Generate a link such that when clicked, the user is added to the workspace. // Generate a link such that when clicked, the user is added to the workspace.
insert_workspace_invitation( insert_workspace_invitation(