chore: revoke for apple user
This commit is contained in:
parent
7c36c712c8
commit
25cec9982f
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
.await
|
||||||
{
|
.map_err(AppResponseError::from)?;
|
||||||
tracing::warn!("revoke apple token failed: {:?}", err);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Ok(AppResponse::Ok().into())
|
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 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?;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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?)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue