feat: call workspace delete when deleting user
This commit is contained in:
parent
943cdee7ad
commit
14a1382d1c
132
src/api/user.rs
132
src/api/user.rs
|
|
@ -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
|
||||
),
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod user_delete;
|
||||
pub mod user_info;
|
||||
pub mod user_init;
|
||||
pub mod user_verify;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
))
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue