mirror of
https://github.com/AppFlowy-IO/AppFlowy-Cloud.git
synced 2025-04-19 03:24:42 -04:00
Merge pull request #780 from AppFlowy-IO/feat/delete-user
Feat/delete user
This commit is contained in:
commit
8015e34841
20 changed files with 354 additions and 42 deletions
|
@ -278,3 +278,18 @@ pub async fn verify_token_cloud(
|
|||
let _: SignInTokenResponse = from_json_response(resp).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_current_user(
|
||||
access_token: &str,
|
||||
appflowy_cloud_base_url: &str,
|
||||
) -> Result<(), Error> {
|
||||
let http_client = reqwest::Client::new();
|
||||
let url = format!("{}/api/user", appflowy_cloud_base_url);
|
||||
let resp = http_client
|
||||
.delete(url)
|
||||
.header("Authorization", format!("Bearer {}", access_token))
|
||||
.send()
|
||||
.await?;
|
||||
check_response(resp).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -27,13 +27,6 @@ pub struct WorkspaceMember {
|
|||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct WorkspaceUsageLimit {
|
||||
pub total_blob_size: i64,
|
||||
pub single_blob_size: i64,
|
||||
pub member_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct WorkspaceBlobUsage {
|
||||
pub consumed_capacity: u64,
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
use axum::response::{IntoResponse, Response};
|
||||
use serde::Deserialize;
|
||||
use shared_entity::response::AppResponseError;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AppFlowyCloudError {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
NotOk(u16, String), // HTTP status code, payload
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::error::WebApiError;
|
||||
use crate::ext::api::{
|
||||
accept_workspace_invitation, invite_user_to_workspace, leave_workspace, verify_token_cloud,
|
||||
accept_workspace_invitation, delete_current_user, invite_user_to_workspace, leave_workspace,
|
||||
verify_token_cloud,
|
||||
};
|
||||
use crate::models::{
|
||||
WebApiAdminCreateUserRequest, WebApiChangePasswordRequest, WebApiCreateSSOProviderRequest,
|
||||
|
@ -39,6 +40,7 @@ pub fn router() -> Router<AppState> {
|
|||
.route("/workspace/:workspace_id/leave", post(leave_workspace_handler))
|
||||
.route("/invite/:invite_id/accept", post(invite_accept_handler))
|
||||
.route("/open_app", post(open_app_handler))
|
||||
.route("/delete-account", delete(delete_account_handler))
|
||||
|
||||
// admin
|
||||
.route("/admin/user", post(admin_add_user_handler))
|
||||
|
@ -115,6 +117,15 @@ async fn open_app_handler(session: UserSession) -> Result<HeaderMap, WebApiError
|
|||
Ok(htmx_redirect(&app_sign_in_url))
|
||||
}
|
||||
|
||||
/// Delete the user account and all associated data.
|
||||
async fn delete_account_handler(
|
||||
state: State<AppState>,
|
||||
session: UserSession,
|
||||
) -> Result<HeaderMap, WebApiError<'static>> {
|
||||
delete_current_user(&session.token.access_token, &state.appflowy_cloud_url).await?;
|
||||
Ok(htmx_redirect("/web/login"))
|
||||
}
|
||||
|
||||
// Invite another user, this will trigger email sending
|
||||
// to the target user
|
||||
async fn invite_handler(
|
||||
|
|
|
@ -10,6 +10,13 @@
|
|||
>
|
||||
{{ user.email|escape }}
|
||||
</div>
|
||||
<div
|
||||
hx-delete="/web-api/delete-account"
|
||||
hx-confirm="This will erase all data associated with this account. Are you sure?"
|
||||
class="button red"
|
||||
>
|
||||
Delete Account
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="top-menu-bar-right">
|
||||
|
|
5
dev.env
5
dev.env
|
@ -69,6 +69,11 @@ GOTRUE_EXTERNAL_DISCORD_ENABLED=false
|
|||
GOTRUE_EXTERNAL_DISCORD_CLIENT_ID=
|
||||
GOTRUE_EXTERNAL_DISCORD_SECRET=
|
||||
GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI=http://localhost:9999/callback
|
||||
# Apple OAuth2
|
||||
GOTRUE_EXTERNAL_APPLE_ENABLED=false
|
||||
GOTRUE_EXTERNAL_APPLE_CLIENT_ID=
|
||||
GOTRUE_EXTERNAL_APPLE_SECRET=
|
||||
GOTRUE_EXTERNAL_APPLE_REDIRECT_URI=http://localhost:9999/callback
|
||||
|
||||
# File Storage
|
||||
APPFLOWY_S3_USE_MINIO=true
|
||||
|
|
|
@ -317,6 +317,7 @@ pub enum ErrorCode {
|
|||
SqlxArgEncodingError = 1035,
|
||||
InvalidContentType = 1036,
|
||||
SingleUploadLimitExceeded = 1037,
|
||||
AppleRevokeTokenError = 1038,
|
||||
}
|
||||
|
||||
impl ErrorCode {
|
||||
|
|
|
@ -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::FolderView;
|
||||
use client_api_entity::workspace_dto::QueryWorkspaceFolder;
|
||||
use client_api_entity::workspace_dto::QueryWorkspaceParam;
|
||||
|
@ -274,13 +275,18 @@ impl Client {
|
|||
.split('&');
|
||||
|
||||
let mut refresh_token: Option<&str> = None;
|
||||
let mut provider_token: Option<String> = None;
|
||||
let mut provider_refresh_token: Option<String> = None;
|
||||
for param in key_value_pairs {
|
||||
match param.split_once('=') {
|
||||
Some(pair) => {
|
||||
let (k, v) = pair;
|
||||
if k == "refresh_token" {
|
||||
refresh_token = Some(v);
|
||||
break;
|
||||
} else if k == "provider_token" {
|
||||
provider_token = Some(v.to_string());
|
||||
} else if k == "provider_refresh_token" {
|
||||
provider_refresh_token = Some(v.to_string());
|
||||
}
|
||||
},
|
||||
None => warn!("param is not in key=value format: {}", param),
|
||||
|
@ -288,13 +294,18 @@ impl Client {
|
|||
}
|
||||
let refresh_token = refresh_token.ok_or(url_missing_param("refresh_token"))?;
|
||||
|
||||
let new_token = self
|
||||
let mut new_token = self
|
||||
.gotrue_client
|
||||
.token(&Grant::RefreshToken(RefreshTokenGrant {
|
||||
refresh_token: refresh_token.to_owned(),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
// refresh endpoint does not return provider token
|
||||
// so we need to set it manually to preserve this information
|
||||
new_token.provider_access_token = provider_token;
|
||||
new_token.provider_refresh_token = provider_refresh_token;
|
||||
|
||||
let (_user, new) = self.verify_token(&new_token.access_token).await?;
|
||||
self.token.write().set(new_token);
|
||||
Ok(new)
|
||||
|
@ -771,6 +782,37 @@ impl Client {
|
|||
AppResponse::<()>::from_response(resp).await?.into_error()
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
|
||||
pub async fn get_snapshot_list(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
|
|
|
@ -47,18 +47,26 @@ impl Action for RefreshTokenAction {
|
|||
if let (Some(token), Some(gotrue_client)) =
|
||||
(weak_token.upgrade(), weak_gotrue_client.upgrade())
|
||||
{
|
||||
let refresh_token = token
|
||||
.read()
|
||||
.as_ref()
|
||||
.ok_or(GoTrueError::NotLoggedIn(
|
||||
let (refresh_token, provider_access_token, provider_refresh_token) = {
|
||||
let mut token_write = token.write();
|
||||
let gotrue_resp_token = token_write.as_mut().ok_or(GoTrueError::NotLoggedIn(
|
||||
"fail to refresh user token".to_owned(),
|
||||
))?
|
||||
.refresh_token
|
||||
.as_str()
|
||||
.to_owned();
|
||||
let access_token_resp = gotrue_client
|
||||
))?;
|
||||
let refresh_token = gotrue_resp_token.refresh_token.as_str().to_owned();
|
||||
let provider_access_token = gotrue_resp_token.provider_access_token.take();
|
||||
let provider_refresh_token = gotrue_resp_token.provider_refresh_token.take();
|
||||
(refresh_token, provider_access_token, provider_refresh_token)
|
||||
};
|
||||
|
||||
let mut access_token_resp = gotrue_client
|
||||
.token(&Grant::RefreshToken(RefreshTokenGrant { refresh_token }))
|
||||
.await?;
|
||||
|
||||
// refresh does not preserve provider token and refresh token
|
||||
// so we need to set it manually to preserve this information
|
||||
access_token_resp.provider_access_token = provider_access_token;
|
||||
access_token_resp.provider_refresh_token = provider_refresh_token;
|
||||
|
||||
token.write().set(access_token_resp);
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
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;
|
||||
|
@ -6,7 +7,7 @@ use actix_web::Result;
|
|||
use actix_web::{web, Scope};
|
||||
use authentication::jwt::{Authorization, UserUuid};
|
||||
use database_entity::dto::{AFUserProfile, AFUserWorkspaceInfo};
|
||||
use shared_entity::dto::auth_dto::{SignInTokenResponse, UpdateUserParams};
|
||||
use shared_entity::dto::auth_dto::{DeleteUserQuery, SignInTokenResponse, UpdateUserParams};
|
||||
use shared_entity::response::AppResponseError;
|
||||
use shared_entity::response::{AppResponse, JsonAppResponse};
|
||||
|
||||
|
@ -16,6 +17,7 @@ pub fn user_scope() -> Scope {
|
|||
.service(web::resource("/update").route(web::post().to(update_user_handler)))
|
||||
.service(web::resource("/profile").route(web::get().to(get_user_profile_handler)))
|
||||
.service(web::resource("/workspace").route(web::get().to(get_user_workspace_info_handler)))
|
||||
.service(web::resource("").route(web::delete().to(delete_user_handler)))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state, path), err)]
|
||||
|
@ -61,3 +63,29 @@ async fn update_user_handler(
|
|||
update_user(&state.pg_pool, auth.uuid()?, params).await?;
|
||||
Ok(AppResponse::Ok().into())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state), err)]
|
||||
async fn delete_user_handler(
|
||||
auth: Authorization,
|
||||
state: Data<AppState>,
|
||||
query: web::Query<DeleteUserQuery>,
|
||||
) -> Result<JsonAppResponse<()>, actix_web::Error> {
|
||||
let user_uuid = auth.uuid()?;
|
||||
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?;
|
||||
Ok(AppResponse::Ok().into())
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod user_delete;
|
||||
pub mod user_info;
|
||||
pub mod user_init;
|
||||
pub mod user_verify;
|
||||
|
|
152
src/biz/user/user_delete.rs
Normal file
152
src/biz/user/user_delete.rs
Normal file
|
@ -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
|
||||
),
|
||||
))
|
||||
}
|
|
@ -39,9 +39,9 @@ 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
|
||||
|
@ -49,7 +49,7 @@ pub async fn delete_workspace_for_user(
|
|||
.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.
|
||||
|
@ -347,7 +347,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 =
|
||||
|
|
|
@ -22,6 +22,7 @@ pub struct Config {
|
|||
pub grpc_history: GrpcHistorySetting,
|
||||
pub collab: CollabSetting,
|
||||
pub mailer: MailerSetting,
|
||||
pub apple_oauth: AppleOAuthSetting,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
|
@ -32,6 +33,12 @@ pub struct MailerSetting {
|
|||
pub smtp_password: Secret<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct AppleOAuthSetting {
|
||||
pub client_id: String,
|
||||
pub client_secret: Secret<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
pub struct CasbinSetting {
|
||||
pub pool_size: u32,
|
||||
|
@ -204,6 +211,10 @@ pub fn get_configuration() -> Result<Config, anyhow::Error> {
|
|||
smtp_username: get_env_var("APPFLOWY_MAILER_SMTP_USERNAME", "sender@example.com"),
|
||||
smtp_password: get_env_var("APPFLOWY_MAILER_SMTP_PASSWORD", "password").into(),
|
||||
},
|
||||
apple_oauth: AppleOAuthSetting {
|
||||
client_id: get_env_var("APPFLOWY_APPLE_OAUTH_CLIENT_ID", ""),
|
||||
client_secret: get_env_var("APPFLOWY_APPLE_OAUTH_CLIENT_SECRET", "").into(),
|
||||
},
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -1,6 +1,35 @@
|
|||
use client_api_test::*;
|
||||
use gotrue::params::{AdminDeleteUserParams, AdminUserParams};
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_delete_self() {
|
||||
let (client, user) = generate_unique_registered_user_client().await;
|
||||
let admin_client = admin_user_client().await;
|
||||
{
|
||||
// user found before deletion
|
||||
let search_result = admin_client
|
||||
.admin_list_users(Some(&user.email))
|
||||
.await
|
||||
.unwrap();
|
||||
let _target_user = search_result
|
||||
.into_iter()
|
||||
.find(|u| u.email == user.email)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
client.delete_user().await.unwrap();
|
||||
|
||||
{
|
||||
// user cannot be found after deletion
|
||||
let search_result = admin_client
|
||||
.admin_list_users(Some(&user.email))
|
||||
.await
|
||||
.unwrap();
|
||||
let target_user = search_result.into_iter().find(|u| u.email == user.email);
|
||||
assert!(target_user.is_none(), "User should be deleted: {:?}", user);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_delete_create_same_user_hard() {
|
||||
let (client, user) = generate_unique_registered_user_client().await;
|
||||
|
|
Loading…
Add table
Reference in a new issue