From 25cec9982f41f625e5a285d79e60c1b17c391887 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 2 Sep 2024 17:25:21 +0800 Subject: [PATCH] chore: revoke for apple user --- ...22939e80ec2e75747503f0493d081ba43e4bd.json | 14 ---- libs/client-api/src/http.rs | 21 ++++- libs/database/src/user.rs | 21 ----- libs/gotrue/src/api.rs | 2 +- libs/shared-entity/src/dto/auth_dto.rs | 6 ++ src/api/user.rs | 80 +++++++++++++++---- src/application.rs | 10 ++- src/biz/user/user_info.rs | 4 - src/biz/workspace/ops.rs | 2 +- src/state.rs | 9 ++- 10 files changed, 105 insertions(+), 64 deletions(-) delete mode 100644 .sqlx/query-e6acbe78f0e8f776901c560088222939e80ec2e75747503f0493d081ba43e4bd.json diff --git a/.sqlx/query-e6acbe78f0e8f776901c560088222939e80ec2e75747503f0493d081ba43e4bd.json b/.sqlx/query-e6acbe78f0e8f776901c560088222939e80ec2e75747503f0493d081ba43e4bd.json deleted file mode 100644 index 2bb9c80d..00000000 --- a/.sqlx/query-e6acbe78f0e8f776901c560088222939e80ec2e75747503f0493d081ba43e4bd.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM auth.users WHERE id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "e6acbe78f0e8f776901c560088222939e80ec2e75747503f0493d081ba43e4bd" -} diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index fd944373..acd532ff 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -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::QueryWorkspaceParam; use client_api_entity::AuthProvider; use client_api_entity::CollabType; @@ -738,15 +739,33 @@ impl Client { AppResponse::<()>::from_response(resp).await?.into_error() } - /// Deletes the user account and all associated data. #[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() } diff --git a/libs/database/src/user.rs b/libs/database/src/user.rs index 599caa84..4ea6f7c9 100644 --- a/libs/database/src/user.rs +++ b/libs/database/src/user.rs @@ -240,24 +240,3 @@ pub async fn select_name_from_uuid(pool: &PgPool, user_uuid: &Uuid) -> Result Result<(), AppError> { - let res = sqlx::query!( - r#" - DELETE FROM auth.users WHERE id = $1 - "#, - user_uuid - ) - .execute(pool) - .await?; - - if res.rows_affected() != 1 { - return Err(AppError::RecordNotFound(format!( - "User with UUID {} not found", - user_uuid - ))); - } - - Ok(()) -} diff --git a/libs/gotrue/src/api.rs b/libs/gotrue/src/api.rs index cfd77fbb..14ac8137 100644 --- a/libs/gotrue/src/api.rs +++ b/libs/gotrue/src/api.rs @@ -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, diff --git a/libs/shared-entity/src/dto/auth_dto.rs b/libs/shared-entity/src/dto/auth_dto.rs index 464a6f21..6f5a270d 100644 --- a/libs/shared-entity/src/dto/auth_dto.rs +++ b/libs/shared-entity/src/dto/auth_dto.rs @@ -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, + pub provider_refresh_token: Option, +} diff --git a/src/api/user.rs b/src/api/user.rs index 025bfdee..98a10e9a 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -1,4 +1,4 @@ -use crate::biz::user::user_info::{delete_user, get_profile, get_user_workspace_info, update_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}; @@ -7,8 +7,9 @@ use actix_web::{web, Scope}; use app_error::ErrorCode; use authentication::jwt::{Authorization, UserUuid}; use database_entity::dto::{AFUserProfile, AFUserWorkspaceInfo}; -use secrecy::ExposeSecret; -use shared_entity::dto::auth_dto::{SignInTokenResponse, UpdateUserParams}; +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}; @@ -69,24 +70,69 @@ async fn update_user_handler( async fn delete_user_handler( auth: Authorization, state: Data, -) -> Result> { - delete_user(&state.pg_pool, auth.uuid()?).await?; - - if is_apple_user(auth) { - if let Err(err) = revoke_apple_token( + query: web::Query, +) -> Result, actix_web::Error> { + let user_uuid = auth.uuid()?; + if is_apple_user(&auth) { + let query = query.into_inner(); + revoke_apple_user( &state.config.apple_oauth.client_id, - state.config.apple_oauth.client_secret.expose_secret(), - "TODO: get original apple during oauth", + &state.config.apple_oauth.client_secret, + query.provider_access_token, + query.provider_refresh_token, + ) + .await?; + } + + 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 - { - tracing::warn!("revoke apple token failed: {:?}", err); - }; - } + .map_err(AppResponseError::from)?; + Ok(AppResponse::Ok().into()) } -fn is_apple_user(auth: Authorization) -> bool { +async fn revoke_apple_user( + client_id: &str, + client_secret: &Secret, + apple_access_token: Option, + apple_refresh_token: Option, +) -> 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; @@ -107,10 +153,11 @@ fn is_apple_user(auth: Authorization) -> bool { } /// Based on: https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens -async fn revoke_apple_token( +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") @@ -118,6 +165,7 @@ async fn revoke_apple_token( ("client_id", apple_client_id), ("client_secret", apple_client_secret), ("token", apple_user_token), + ("token_type_hint", token_type_hint), ]) .send() .await?; diff --git a/src/application.rs b/src/application.rs index 6086c9c8..35b65cf8 100644 --- a/src/application.rs +++ b/src/application.rs @@ -216,7 +216,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result Result Result { 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 { diff --git a/src/biz/user/user_info.rs b/src/biz/user/user_info.rs index 6ae58dad..470e39a4 100644 --- a/src/biz/user/user_info.rs +++ b/src/biz/user/user_info.rs @@ -74,7 +74,3 @@ pub async fn update_user( let metadata = params.metadata.map(|m| json!(m.into_inner())); Ok(database::user::update_user(pg_pool, &user_uuid, params.name, params.email, metadata).await?) } - -pub async fn delete_user(pg_pool: &PgPool, user_uuid: Uuid) -> Result<(), AppResponseError> { - Ok(database::user::delete_user(pg_pool, &user_uuid).await?) -} diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index 052ba768..0580ceb2 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -346,7 +346,7 @@ pub async fn invite_workspace_members( .begin() .await .context("Begin transaction to invite workspace members")?; - let admin_token = gotrue_admin.token(gotrue_client).await?; + let admin_token = gotrue_admin.token().await?; let inviter_name = database::user::select_name_from_uuid(pg_pool, inviter).await?; let workspace_name = diff --git a/src/state.rs b/src/state.rs index 45bb34a7..29730804 100644 --- a/src/state.rs +++ b/src/state.rs @@ -150,20 +150,23 @@ impl AppMetrics { #[derive(Debug, Clone)] pub struct GoTrueAdmin { + pub gotrue_client: gotrue::api::Client, pub admin_email: String, pub password: Secret, } impl GoTrueAdmin { - pub fn new(admin_email: String, password: String) -> Self { + pub fn new(admin_email: String, password: String, gotrue_client: gotrue::api::Client) -> Self { Self { admin_email, password: password.into(), + gotrue_client, } } - pub async fn token(&self, client: &gotrue::api::Client) -> Result { - let token = client + pub async fn token(&self) -> Result { + let token = self + .gotrue_client .token(&Grant::Password(PasswordGrant { email: self.admin_email.clone(), password: self.password.expose_secret().clone(),