Merge pull request #407 from AppFlowy-IO/admin-frontend/email-invite
Admin frontend/email invite
This commit is contained in:
commit
68b0769bf0
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue