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

View File

@ -240,24 +240,3 @@ pub async fn select_name_from_uuid(pool: &PgPool, user_uuid: &Uuid) -> Result<St
.await?;
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 tracing::event;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Client {
client: reqwest::Client,
pub base_url: String,

View File

@ -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<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::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<AppState>,
) -> Result<JsonAppResponse<()>> {
delete_user(&state.pg_pool, auth.uuid()?).await?;
if is_apple_user(auth) {
if let Err(err) = revoke_apple_token(
query: web::Query<DeleteUserQuery>,
) -> Result<JsonAppResponse<()>, 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<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;
@ -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?;

View File

@ -216,7 +216,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
// Gotrue
info!("Connecting to GoTrue...");
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
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(
gotrue_client: &gotrue::api::Client,
gotrue_client: gotrue::api::Client,
pg_pool: &PgPool,
gotrue_setting: &GoTrueSetting,
) -> Result<GoTrueAdmin, Error> {
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 {

View File

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

View File

@ -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 =

View File

@ -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<String>,
}
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<String, AppError> {
let token = client
pub async fn token(&self) -> Result<String, AppError> {
let token = self
.gotrue_client
.token(&Grant::Password(PasswordGrant {
email: self.admin_email.clone(),
password: self.password.expose_secret().clone(),