Skip to content

Commit

Permalink
Merge pull request #780 from AppFlowy-IO/feat/delete-user
Browse files Browse the repository at this point in the history
Feat/delete user
  • Loading branch information
speed2exe authored Sep 3, 2024
2 parents 5fe1a87 + 14a1382 commit 8015e34
Show file tree
Hide file tree
Showing 20 changed files with 354 additions and 42 deletions.
15 changes: 15 additions & 0 deletions admin_frontend/src/ext/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,18 @@ pub async fn verify_token_cloud(
let _: SignInTokenResponse = from_json_response(resp).await?;
Ok(())
}

pub async fn delete_current_user(
access_token: &str,
appflowy_cloud_base_url: &str,
) -> Result<(), Error> {
let http_client = reqwest::Client::new();
let url = format!("{}/api/user", appflowy_cloud_base_url);
let resp = http_client
.delete(url)
.header("Authorization", format!("Bearer {}", access_token))
.send()
.await?;
check_response(resp).await?;
Ok(())
}
7 changes: 0 additions & 7 deletions admin_frontend/src/ext/entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,6 @@ pub struct WorkspaceMember {
pub role: String,
}

#[derive(Deserialize)]
pub struct WorkspaceUsageLimit {
pub total_blob_size: i64,
pub single_blob_size: i64,
pub member_count: i64,
}

#[derive(Deserialize, Serialize)]
pub struct WorkspaceBlobUsage {
pub consumed_capacity: u64,
Expand Down
7 changes: 0 additions & 7 deletions admin_frontend/src/ext/error.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
use axum::response::{IntoResponse, Response};
use serde::Deserialize;
use shared_entity::response::AppResponseError;

#[derive(Deserialize, Debug)]
pub struct AppFlowyCloudError {
pub code: String,
pub message: String,
}

#[derive(Debug)]
pub enum Error {
NotOk(u16, String), // HTTP status code, payload
Expand Down
13 changes: 12 additions & 1 deletion admin_frontend/src/web_api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::error::WebApiError;
use crate::ext::api::{
accept_workspace_invitation, invite_user_to_workspace, leave_workspace, verify_token_cloud,
accept_workspace_invitation, delete_current_user, invite_user_to_workspace, leave_workspace,
verify_token_cloud,
};
use crate::models::{
WebApiAdminCreateUserRequest, WebApiChangePasswordRequest, WebApiCreateSSOProviderRequest,
Expand Down Expand Up @@ -39,6 +40,7 @@ pub fn router() -> Router<AppState> {
.route("/workspace/:workspace_id/leave", post(leave_workspace_handler))
.route("/invite/:invite_id/accept", post(invite_accept_handler))
.route("/open_app", post(open_app_handler))
.route("/delete-account", delete(delete_account_handler))

// admin
.route("/admin/user", post(admin_add_user_handler))
Expand Down Expand Up @@ -115,6 +117,15 @@ async fn open_app_handler(session: UserSession) -> Result<HeaderMap, WebApiError
Ok(htmx_redirect(&app_sign_in_url))
}

/// Delete the user account and all associated data.
async fn delete_account_handler(
state: State<AppState>,
session: UserSession,
) -> Result<HeaderMap, WebApiError<'static>> {
delete_current_user(&session.token.access_token, &state.appflowy_cloud_url).await?;
Ok(htmx_redirect("/web/login"))
}

// Invite another user, this will trigger email sending
// to the target user
async fn invite_handler(
Expand Down
7 changes: 7 additions & 0 deletions admin_frontend/templates/components/top_menu_bar.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ <h2>&nbsp; AppFlowy Cloud &nbsp;</h2>
>
{{ user.email|escape }}
</div>
<div
hx-delete="/web-api/delete-account"
hx-confirm="This will erase all data associated with this account. Are you sure?"
class="button red"
>
Delete Account
</div>
</div>

<div id="top-menu-bar-right">
Expand Down
5 changes: 5 additions & 0 deletions dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ GOTRUE_EXTERNAL_DISCORD_ENABLED=false
GOTRUE_EXTERNAL_DISCORD_CLIENT_ID=
GOTRUE_EXTERNAL_DISCORD_SECRET=
GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI=http://localhost:9999/callback
# Apple OAuth2
GOTRUE_EXTERNAL_APPLE_ENABLED=false
GOTRUE_EXTERNAL_APPLE_CLIENT_ID=
GOTRUE_EXTERNAL_APPLE_SECRET=
GOTRUE_EXTERNAL_APPLE_REDIRECT_URI=http://localhost:9999/callback

# File Storage
APPFLOWY_S3_USE_MINIO=true
Expand Down
1 change: 1 addition & 0 deletions libs/app-error/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ pub enum ErrorCode {
SqlxArgEncodingError = 1035,
InvalidContentType = 1036,
SingleUploadLimitExceeded = 1037,
AppleRevokeTokenError = 1038,
}

impl ErrorCode {
Expand Down
46 changes: 44 additions & 2 deletions libs/client-api/src/http.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::notify::{ClientToken, TokenStateReceiver};
use app_error::AppError;
use client_api_entity::auth_dto::DeleteUserQuery;
use client_api_entity::workspace_dto::FolderView;
use client_api_entity::workspace_dto::QueryWorkspaceFolder;
use client_api_entity::workspace_dto::QueryWorkspaceParam;
Expand Down Expand Up @@ -274,27 +275,37 @@ impl Client {
.split('&');

let mut refresh_token: Option<&str> = None;
let mut provider_token: Option<String> = None;
let mut provider_refresh_token: Option<String> = None;
for param in key_value_pairs {
match param.split_once('=') {
Some(pair) => {
let (k, v) = pair;
if k == "refresh_token" {
refresh_token = Some(v);
break;
} else if k == "provider_token" {
provider_token = Some(v.to_string());
} else if k == "provider_refresh_token" {
provider_refresh_token = Some(v.to_string());
}
},
None => warn!("param is not in key=value format: {}", param),
}
}
let refresh_token = refresh_token.ok_or(url_missing_param("refresh_token"))?;

let new_token = self
let mut new_token = self
.gotrue_client
.token(&Grant::RefreshToken(RefreshTokenGrant {
refresh_token: refresh_token.to_owned(),
}))
.await?;

// refresh endpoint does not return provider token
// so we need to set it manually to preserve this information
new_token.provider_access_token = provider_token;
new_token.provider_refresh_token = provider_refresh_token;

let (_user, new) = self.verify_token(&new_token.access_token).await?;
self.token.write().set(new_token);
Ok(new)
Expand Down Expand Up @@ -771,6 +782,37 @@ impl Client {
AppResponse::<()>::from_response(resp).await?.into_error()
}

#[instrument(level = "info", skip_all, err)]
pub async fn delete_user(&self) -> Result<(), AppResponseError> {
let (provider_access_token, provider_refresh_token) = {
let token = self.token();
let token_read = token.read();
let token_resp = token_read
.as_ref()
.ok_or(AppResponseError::from(AppError::NotLoggedIn(
"token is empty".to_string(),
)))?;
(
token_resp.provider_access_token.clone(),
token_resp.provider_refresh_token.clone(),
)
};

let url = format!("{}/api/user", self.base_url);
let resp = self
.http_client_with_auth(Method::DELETE, &url)
.await?
.query(&DeleteUserQuery {
provider_access_token,
provider_refresh_token,
})
.send()
.await?;

log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}

pub async fn get_snapshot_list(
&self,
workspace_id: &str,
Expand Down
26 changes: 17 additions & 9 deletions libs/client-api/src/native/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,26 @@ impl Action for RefreshTokenAction {
if let (Some(token), Some(gotrue_client)) =
(weak_token.upgrade(), weak_gotrue_client.upgrade())
{
let refresh_token = token
.read()
.as_ref()
.ok_or(GoTrueError::NotLoggedIn(
let (refresh_token, provider_access_token, provider_refresh_token) = {
let mut token_write = token.write();
let gotrue_resp_token = token_write.as_mut().ok_or(GoTrueError::NotLoggedIn(
"fail to refresh user token".to_owned(),
))?
.refresh_token
.as_str()
.to_owned();
let access_token_resp = gotrue_client
))?;
let refresh_token = gotrue_resp_token.refresh_token.as_str().to_owned();
let provider_access_token = gotrue_resp_token.provider_access_token.take();
let provider_refresh_token = gotrue_resp_token.provider_refresh_token.take();
(refresh_token, provider_access_token, provider_refresh_token)
};

let mut access_token_resp = gotrue_client
.token(&Grant::RefreshToken(RefreshTokenGrant { refresh_token }))
.await?;

// refresh does not preserve provider token and refresh token
// so we need to set it manually to preserve this information
access_token_resp.provider_access_token = provider_access_token;
access_token_resp.provider_refresh_token = provider_refresh_token;

token.write().set(access_token_resp);
}
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion libs/gotrue/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use infra::reqwest::{check_response, from_body, from_response};
use reqwest::{Method, RequestBuilder};
use tracing::event;

#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Client {
client: reqwest::Client,
pub base_url: String,
Expand Down
6 changes: 6 additions & 0 deletions libs/shared-entity/src/dto/auth_dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,9 @@ pub struct SignInPasswordResponse {
pub struct SignInTokenResponse {
pub is_new: bool,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct DeleteUserQuery {
pub provider_access_token: Option<String>,
pub provider_refresh_token: Option<String>,
}
30 changes: 29 additions & 1 deletion src/api/user.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::biz::user::user_delete::delete_user;
use crate::biz::user::user_info::{get_profile, get_user_workspace_info, update_user};
use crate::biz::user::user_verify::verify_token;
use crate::state::AppState;
Expand All @@ -6,7 +7,7 @@ use actix_web::Result;
use actix_web::{web, Scope};
use authentication::jwt::{Authorization, UserUuid};
use database_entity::dto::{AFUserProfile, AFUserWorkspaceInfo};
use shared_entity::dto::auth_dto::{SignInTokenResponse, UpdateUserParams};
use shared_entity::dto::auth_dto::{DeleteUserQuery, SignInTokenResponse, UpdateUserParams};
use shared_entity::response::AppResponseError;
use shared_entity::response::{AppResponse, JsonAppResponse};

Expand All @@ -16,6 +17,7 @@ pub fn user_scope() -> Scope {
.service(web::resource("/update").route(web::post().to(update_user_handler)))
.service(web::resource("/profile").route(web::get().to(get_user_profile_handler)))
.service(web::resource("/workspace").route(web::get().to(get_user_workspace_info_handler)))
.service(web::resource("").route(web::delete().to(delete_user_handler)))
}

#[tracing::instrument(skip(state, path), err)]
Expand Down Expand Up @@ -61,3 +63,29 @@ async fn update_user_handler(
update_user(&state.pg_pool, auth.uuid()?, params).await?;
Ok(AppResponse::Ok().into())
}

#[tracing::instrument(skip(state), err)]
async fn delete_user_handler(
auth: Authorization,
state: Data<AppState>,
query: web::Query<DeleteUserQuery>,
) -> Result<JsonAppResponse<()>, actix_web::Error> {
let user_uuid = auth.uuid()?;
let DeleteUserQuery {
provider_access_token,
provider_refresh_token,
} = query.into_inner();
delete_user(
&state.pg_pool,
&state.bucket_storage,
&state.gotrue_client,
&state.gotrue_admin,
&state.config.apple_oauth,
auth,
user_uuid,
provider_access_token,
provider_refresh_token,
)
.await?;
Ok(AppResponse::Ok().into())
}
9 changes: 6 additions & 3 deletions src/api/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,13 @@ async fn delete_workspace_handler(
workspace_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<Json<AppResponse<()>>> {
let bucket_storage = &state.bucket_storage;

// TODO: add permission for workspace deletion
workspace::ops::delete_workspace_for_user(&state.pg_pool, &workspace_id, bucket_storage).await?;
workspace::ops::delete_workspace_for_user(
state.pg_pool.clone(),
*workspace_id,
state.bucket_storage.clone(),
)
.await?;
Ok(AppResponse::Ok().into())
}

Expand Down
10 changes: 7 additions & 3 deletions src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
// Gotrue
info!("Connecting to GoTrue...");
let gotrue_client = get_gotrue_client(&config.gotrue).await?;
let gotrue_admin = setup_admin_account(&gotrue_client, &pg_pool, &config.gotrue).await?;
let gotrue_admin = setup_admin_account(gotrue_client.clone(), &pg_pool, &config.gotrue).await?;

// Redis
info!("Connecting to Redis...");
Expand Down Expand Up @@ -320,13 +320,17 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
}

async fn setup_admin_account(
gotrue_client: &gotrue::api::Client,
gotrue_client: gotrue::api::Client,
pg_pool: &PgPool,
gotrue_setting: &GoTrueSetting,
) -> Result<GoTrueAdmin, Error> {
let admin_email = gotrue_setting.admin_email.as_str();
let password = gotrue_setting.admin_password.expose_secret();
let gotrue_admin = GoTrueAdmin::new(admin_email.to_owned(), password.to_owned());
let gotrue_admin = GoTrueAdmin::new(
admin_email.to_owned(),
password.to_owned(),
gotrue_client.clone(),
);

match gotrue_client
.token(&Grant::Password(PasswordGrant {
Expand Down
1 change: 1 addition & 0 deletions src/biz/user/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod user_delete;
pub mod user_info;
pub mod user_init;
pub mod user_verify;
Loading

0 comments on commit 8015e34

Please sign in to comment.