Skip to content

Commit

Permalink
feat: call workspace delete when deleting user
Browse files Browse the repository at this point in the history
  • Loading branch information
speed2exe committed Sep 3, 2024
1 parent 943cdee commit 14a1382
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 122 deletions.
132 changes: 17 additions & 115 deletions src/api/user.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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)?;

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,
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
{
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
),
))
.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
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;
152 changes: 152 additions & 0 deletions src/biz/user/user_delete.rs
Original file line number Diff line number Diff line change
@@ -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
),
))
}
8 changes: 4 additions & 4 deletions src/biz/workspace/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,17 @@ 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
.remove_dir(workspace_id.to_string().as_str())
.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.
Expand Down

0 comments on commit 14a1382

Please sign in to comment.