feat: call workspace delete when deleting user

This commit is contained in:
Zack Fu Zi Xiang 2024-09-03 10:49:53 +08:00
parent 943cdee7ad
commit 14a1382d1c
No known key found for this signature in database
5 changed files with 180 additions and 122 deletions

View File

@ -1,14 +1,12 @@
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;
use actix_web::web::{Data, Json};
use actix_web::Result;
use actix_web::{web, Scope};
use app_error::ErrorCode;
use authentication::jwt::{Authorization, UserUuid};
use database_entity::dto::{AFUserProfile, AFUserWorkspaceInfo};
use gotrue::params::AdminDeleteUserParams;
use secrecy::{ExposeSecret, Secret};
use shared_entity::dto::auth_dto::{DeleteUserQuery, SignInTokenResponse, UpdateUserParams};
use shared_entity::response::AppResponseError;
use shared_entity::response::{AppResponse, JsonAppResponse};
@ -73,117 +71,21 @@ async fn delete_user_handler(
query: web::Query<DeleteUserQuery>,
) -> Result<JsonAppResponse<()>, actix_web::Error> {
let user_uuid = auth.uuid()?;
if is_apple_user(&auth) {
let query = query.into_inner();
if let Err(err) = revoke_apple_user(
&state.config.apple_oauth.client_id,
&state.config.apple_oauth.client_secret,
query.provider_access_token,
query.provider_refresh_token,
)
.await
{
tracing::warn!("revoke apple user failed: {:?}", err);
};
}
let admin_token = state.gotrue_admin.token().await?;
let _ = &state
.gotrue_client
.admin_delete_user(
&admin_token,
&user_uuid.to_string(),
&AdminDeleteUserParams {
should_soft_delete: false,
},
)
.await
.map_err(AppResponseError::from)?;
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())
}
async fn revoke_apple_user(
client_id: &str,
client_secret: &Secret<String>,
apple_access_token: Option<String>,
apple_refresh_token: Option<String>,
) -> Result<(), AppResponseError> {
let (type_type_hint, token) = match apple_access_token {
Some(access_token) => ("access_token", access_token),
None => match apple_refresh_token {
Some(refresh_token) => ("refresh_token", refresh_token),
None => {
return Err(AppResponseError::new(
ErrorCode::InvalidRequest,
"apple email deletion must provide access_token or refresh_token",
))
},
},
};
if let Err(err) = revoke_apple_token_http_call(
client_id,
client_secret.expose_secret(),
&token,
type_type_hint,
)
.await
{
tracing::warn!("revoke apple token failed: {:?}", err);
};
Ok(())
}
fn is_apple_user(auth: &Authorization) -> bool {
if let Some(provider) = auth.claims.app_metadata.get("provider") {
if provider == "apple" {
return true;
}
};
if let Some(providers) = auth.claims.app_metadata.get("providers") {
if let Some(providers) = providers.as_array() {
for provider in providers {
if provider == "apple" {
return true;
}
}
}
}
false
}
/// Based on: https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens
async fn revoke_apple_token_http_call(
apple_client_id: &str,
apple_client_secret: &str,
apple_user_token: &str,
token_type_hint: &str,
) -> Result<(), AppResponseError> {
let resp = reqwest::Client::new()
.post("https://appleid.apple.com/auth/revoke")
.form(&[
("client_id", apple_client_id),
("client_secret", apple_client_secret),
("token", apple_user_token),
("token_type_hint", token_type_hint),
])
.send()
.await?;
let status = resp.status();
if status.is_success() {
return Ok(());
}
let payload = resp.text().await?;
Err(AppResponseError::new(
ErrorCode::AppleRevokeTokenError,
format!(
"calling apple revoke, code: {}, message: {}",
status, payload
),
))
}

View File

@ -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())
}

View File

@ -1,3 +1,4 @@
pub mod user_delete;
pub mod user_info;
pub mod user_init;
pub mod user_verify;

152
src/biz/user/user_delete.rs Normal file
View File

@ -0,0 +1,152 @@
use std::sync::Arc;
use crate::biz::workspace::ops::get_all_user_workspaces;
use crate::state::GoTrueAdmin;
use crate::{biz::workspace::ops::delete_workspace_for_user, config::config::AppleOAuthSetting};
use app_error::ErrorCode;
use authentication::jwt::Authorization;
use database::file::s3_client_impl::S3BucketStorage;
use gotrue::params::AdminDeleteUserParams;
use secrecy::{ExposeSecret, Secret};
use shared_entity::response::AppResponseError;
use uuid::Uuid;
#[allow(clippy::too_many_arguments)]
pub async fn delete_user(
pg_pool: &sqlx::PgPool,
bucket_storage: &Arc<S3BucketStorage>,
gotrue_client: &gotrue::api::Client,
gotrue_admin: &GoTrueAdmin,
apple_oauth: &AppleOAuthSetting,
auth: Authorization,
user_uuid: Uuid,
provider_access_token: Option<String>,
provider_refresh_token: Option<String>,
) -> Result<(), AppResponseError> {
if is_apple_user(&auth) {
if let Err(err) = revoke_apple_user(
&apple_oauth.client_id,
&apple_oauth.client_secret,
provider_access_token,
provider_refresh_token,
)
.await
{
tracing::warn!("revoke apple user failed: {:?}", err);
};
}
let admin_token = gotrue_admin.token().await?;
gotrue_client
.admin_delete_user(
&admin_token,
&user_uuid.to_string(),
&AdminDeleteUserParams {
should_soft_delete: false,
},
)
.await
.map_err(AppResponseError::from)?;
// spawn tasks to delete all user workspace and object storage
let user_workspaces = get_all_user_workspaces(pg_pool, &user_uuid, false).await?;
let mut tasks = vec![];
for workspace in user_workspaces {
let cloned_pg_pool = pg_pool.clone();
tasks.push(tokio::spawn(delete_workspace_for_user(
cloned_pg_pool,
workspace.workspace_id,
bucket_storage.clone(),
)));
}
for task in tasks {
task.await??;
}
Ok(())
}
async fn revoke_apple_user(
client_id: &str,
client_secret: &Secret<String>,
apple_access_token: Option<String>,
apple_refresh_token: Option<String>,
) -> Result<(), AppResponseError> {
let (type_type_hint, token) = match apple_access_token {
Some(access_token) => ("access_token", access_token),
None => match apple_refresh_token {
Some(refresh_token) => ("refresh_token", refresh_token),
None => {
return Err(AppResponseError::new(
ErrorCode::InvalidRequest,
"apple email deletion must provide access_token or refresh_token",
))
},
},
};
if let Err(err) = revoke_apple_token_http_call(
client_id,
client_secret.expose_secret(),
&token,
type_type_hint,
)
.await
{
tracing::warn!("revoke apple token failed: {:?}", err);
};
Ok(())
}
fn is_apple_user(auth: &Authorization) -> bool {
if let Some(provider) = auth.claims.app_metadata.get("provider") {
if provider == "apple" {
return true;
}
};
if let Some(providers) = auth.claims.app_metadata.get("providers") {
if let Some(providers) = providers.as_array() {
for provider in providers {
if provider == "apple" {
return true;
}
}
}
}
false
}
/// Based on: https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens
async fn revoke_apple_token_http_call(
apple_client_id: &str,
apple_client_secret: &str,
apple_user_token: &str,
token_type_hint: &str,
) -> Result<(), AppResponseError> {
let resp = reqwest::Client::new()
.post("https://appleid.apple.com/auth/revoke")
.form(&[
("client_id", apple_client_id),
("client_secret", apple_client_secret),
("token", apple_user_token),
("token_type_hint", token_type_hint),
])
.send()
.await?;
let status = resp.status();
if status.is_success() {
return Ok(());
}
let payload = resp.text().await?;
Err(AppResponseError::new(
ErrorCode::AppleRevokeTokenError,
format!(
"calling apple revoke, code: {}, message: {}",
status, payload
),
))
}

View File

@ -39,9 +39,9 @@ use crate::state::GoTrueAdmin;
const MAX_COMMENT_LENGTH: usize = 5000;
pub async fn delete_workspace_for_user(
pg_pool: &PgPool,
workspace_id: &Uuid,
bucket_storage: &Arc<S3BucketStorage>,
pg_pool: PgPool,
workspace_id: Uuid,
bucket_storage: Arc<S3BucketStorage>,
) -> Result<(), AppResponseError> {
// remove files from s3
bucket_storage
@ -49,7 +49,7 @@ pub async fn delete_workspace_for_user(
.await?;
// remove from postgres
delete_from_workspace(pg_pool, workspace_id).await?;
delete_from_workspace(&pg_pool, &workspace_id).await?;
// TODO: There can be a rare case where user uploads while workspace is being deleted.
// We need some routine job to clean up these orphaned files.