chore: revoke for apple user

This commit is contained in:
Zack Fu Zi Xiang 2024-09-02 17:25:21 +08:00
parent 7c36c712c8
commit 25cec9982f
No known key found for this signature in database
10 changed files with 105 additions and 64 deletions

View File

@ -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"
}

View File

@ -1,5 +1,6 @@
use crate::notify::{ClientToken, TokenStateReceiver}; use crate::notify::{ClientToken, TokenStateReceiver};
use app_error::AppError; use app_error::AppError;
use client_api_entity::auth_dto::DeleteUserQuery;
use client_api_entity::workspace_dto::QueryWorkspaceParam; use client_api_entity::workspace_dto::QueryWorkspaceParam;
use client_api_entity::AuthProvider; use client_api_entity::AuthProvider;
use client_api_entity::CollabType; use client_api_entity::CollabType;
@ -738,15 +739,33 @@ impl Client {
AppResponse::<()>::from_response(resp).await?.into_error() AppResponse::<()>::from_response(resp).await?.into_error()
} }
/// Deletes the user account and all associated data.
#[instrument(level = "info", skip_all, err)] #[instrument(level = "info", skip_all, err)]
pub async fn delete_user(&self) -> Result<(), AppResponseError> { 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 url = format!("{}/api/user", self.base_url);
let resp = self let resp = self
.http_client_with_auth(Method::DELETE, &url) .http_client_with_auth(Method::DELETE, &url)
.await? .await?
.query(&DeleteUserQuery {
provider_access_token,
provider_refresh_token,
})
.send() .send()
.await?; .await?;
log_request_id(&resp); log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error() AppResponse::<()>::from_response(resp).await?.into_error()
} }

View File

@ -240,24 +240,3 @@ pub async fn select_name_from_uuid(pool: &PgPool, user_uuid: &Uuid) -> Result<St
.await?; .await?;
Ok(email) Ok(email)
} }
#[inline]
pub async fn delete_user(pool: &PgPool, user_uuid: &Uuid) -> 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(())
}

View File

@ -14,7 +14,7 @@ use infra::reqwest::{check_response, from_body, from_response};
use reqwest::{Method, RequestBuilder}; use reqwest::{Method, RequestBuilder};
use tracing::event; use tracing::event;
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct Client { pub struct Client {
client: reqwest::Client, client: reqwest::Client,
pub base_url: String, pub base_url: String,

View File

@ -65,3 +65,9 @@ pub struct SignInPasswordResponse {
pub struct SignInTokenResponse { pub struct SignInTokenResponse {
pub is_new: bool, 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>,
}

View File

@ -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::biz::user::user_verify::verify_token;
use crate::state::AppState; use crate::state::AppState;
use actix_web::web::{Data, Json}; use actix_web::web::{Data, Json};
@ -7,8 +7,9 @@ use actix_web::{web, Scope};
use app_error::ErrorCode; use app_error::ErrorCode;
use authentication::jwt::{Authorization, UserUuid}; use authentication::jwt::{Authorization, UserUuid};
use database_entity::dto::{AFUserProfile, AFUserWorkspaceInfo}; use database_entity::dto::{AFUserProfile, AFUserWorkspaceInfo};
use secrecy::ExposeSecret; use gotrue::params::AdminDeleteUserParams;
use shared_entity::dto::auth_dto::{SignInTokenResponse, UpdateUserParams}; use secrecy::{ExposeSecret, Secret};
use shared_entity::dto::auth_dto::{DeleteUserQuery, SignInTokenResponse, UpdateUserParams};
use shared_entity::response::AppResponseError; use shared_entity::response::AppResponseError;
use shared_entity::response::{AppResponse, JsonAppResponse}; use shared_entity::response::{AppResponse, JsonAppResponse};
@ -69,24 +70,69 @@ async fn update_user_handler(
async fn delete_user_handler( async fn delete_user_handler(
auth: Authorization, auth: Authorization,
state: Data<AppState>, state: Data<AppState>,
) -> Result<JsonAppResponse<()>> { query: web::Query<DeleteUserQuery>,
delete_user(&state.pg_pool, auth.uuid()?).await?; ) -> Result<JsonAppResponse<()>, actix_web::Error> {
let user_uuid = auth.uuid()?;
if is_apple_user(auth) { if is_apple_user(&auth) {
if let Err(err) = revoke_apple_token( let query = query.into_inner();
revoke_apple_user(
&state.config.apple_oauth.client_id, &state.config.apple_oauth.client_id,
state.config.apple_oauth.client_secret.expose_secret(), &state.config.apple_oauth.client_secret,
"TODO: get original apple during oauth", 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
.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,
) )
.await .await
{ {
tracing::warn!("revoke apple token failed: {:?}", err); tracing::warn!("revoke apple token failed: {:?}", err);
}; };
} Ok(())
Ok(AppResponse::Ok().into())
} }
fn is_apple_user(auth: Authorization) -> bool { fn is_apple_user(auth: &Authorization) -> bool {
if let Some(provider) = auth.claims.app_metadata.get("provider") { if let Some(provider) = auth.claims.app_metadata.get("provider") {
if provider == "apple" { if provider == "apple" {
return true; 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 /// 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_id: &str,
apple_client_secret: &str, apple_client_secret: &str,
apple_user_token: &str, apple_user_token: &str,
token_type_hint: &str,
) -> Result<(), AppResponseError> { ) -> Result<(), AppResponseError> {
let resp = reqwest::Client::new() let resp = reqwest::Client::new()
.post("https://appleid.apple.com/auth/revoke") .post("https://appleid.apple.com/auth/revoke")
@ -118,6 +165,7 @@ async fn revoke_apple_token(
("client_id", apple_client_id), ("client_id", apple_client_id),
("client_secret", apple_client_secret), ("client_secret", apple_client_secret),
("token", apple_user_token), ("token", apple_user_token),
("token_type_hint", token_type_hint),
]) ])
.send() .send()
.await?; .await?;

View File

@ -216,7 +216,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
// Gotrue // Gotrue
info!("Connecting to GoTrue..."); info!("Connecting to GoTrue...");
let gotrue_client = get_gotrue_client(&config.gotrue).await?; 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 // Redis
info!("Connecting to Redis..."); info!("Connecting to Redis...");
@ -320,13 +320,17 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
} }
async fn setup_admin_account( async fn setup_admin_account(
gotrue_client: &gotrue::api::Client, gotrue_client: gotrue::api::Client,
pg_pool: &PgPool, pg_pool: &PgPool,
gotrue_setting: &GoTrueSetting, gotrue_setting: &GoTrueSetting,
) -> Result<GoTrueAdmin, Error> { ) -> Result<GoTrueAdmin, Error> {
let admin_email = gotrue_setting.admin_email.as_str(); let admin_email = gotrue_setting.admin_email.as_str();
let password = gotrue_setting.admin_password.expose_secret(); 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 match gotrue_client
.token(&Grant::Password(PasswordGrant { .token(&Grant::Password(PasswordGrant {

View File

@ -74,7 +74,3 @@ pub async fn update_user(
let metadata = params.metadata.map(|m| json!(m.into_inner())); 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?) 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?)
}

View File

@ -346,7 +346,7 @@ pub async fn invite_workspace_members(
.begin() .begin()
.await .await
.context("Begin transaction to invite workspace members")?; .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 inviter_name = database::user::select_name_from_uuid(pg_pool, inviter).await?;
let workspace_name = let workspace_name =

View File

@ -150,20 +150,23 @@ impl AppMetrics {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct GoTrueAdmin { pub struct GoTrueAdmin {
pub gotrue_client: gotrue::api::Client,
pub admin_email: String, pub admin_email: String,
pub password: Secret<String>, pub password: Secret<String>,
} }
impl GoTrueAdmin { 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 { Self {
admin_email, admin_email,
password: password.into(), password: password.into(),
gotrue_client,
} }
} }
pub async fn token(&self, client: &gotrue::api::Client) -> Result<String, AppError> { pub async fn token(&self) -> Result<String, AppError> {
let token = client let token = self
.gotrue_client
.token(&Grant::Password(PasswordGrant { .token(&Grant::Password(PasswordGrant {
email: self.admin_email.clone(), email: self.admin_email.clone(),
password: self.password.expose_secret().clone(), password: self.password.expose_secret().clone(),