feat: User deletion (#177)
* feat: database trigger for hard delete of user * feat: clean delete user data * feat: hard delete when admin deletes
This commit is contained in:
parent
1d07afa304
commit
3890f5d306
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE auth.users\n SET role = 'supabase_admin', email_confirmed_at = NOW()\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "884c44d3a87ca4e520f9e8cec6ba673ea4e196920636e4a4db9d42fad3ef4d73"
|
||||
}
|
||||
|
|
@ -163,7 +163,7 @@ pub async fn admin_delete_user_handler(
|
|||
&session.token.access_token,
|
||||
&user_uuid,
|
||||
&AdminDeleteUserParams {
|
||||
should_soft_delete: true,
|
||||
should_soft_delete: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::notify::{ClientToken, TokenStateReceiver};
|
||||
use anyhow::Context;
|
||||
use gotrue_entity::dto::AuthProvider;
|
||||
use prost::Message as ProstMessage;
|
||||
|
||||
use app_error::AppError;
|
||||
|
|
@ -44,7 +45,7 @@ use url::Url;
|
|||
use crate::retry::{RefreshTokenAction, RefreshTokenRetryCondition};
|
||||
use crate::ws::{WSClientHttpSender, WSError};
|
||||
use gotrue_entity::dto::SignUpResponse::{Authenticated, NotAuthenticated};
|
||||
use gotrue_entity::dto::{AuthProvider, GotrueTokenResponse, UpdateGotrueUserParams, User};
|
||||
use gotrue_entity::dto::{GotrueTokenResponse, UpdateGotrueUserParams, User};
|
||||
use realtime_entity::realtime_proto::HttpRealtimeMessage;
|
||||
|
||||
/// `Client` is responsible for managing communication with the GoTrue API and cloud storage.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.af_user TO supabase_auth_admin;
|
||||
|
||||
-- Trigger Function to delete a user from the pulic.af_user table
|
||||
-- when a user is deleted from auth.users table (with matching uuid) field
|
||||
CREATE OR REPLACE FUNCTION public.delete_user()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
DELETE FROM public.af_user WHERE uuid = OLD.id;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER delete_user_trigger
|
||||
AFTER DELETE ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.delete_user();
|
||||
|
||||
-- Trigger Function to update the 'deleted_at' field in the pulic.af_user table
|
||||
-- (Soft Delete)
|
||||
CREATE OR REPLACE FUNCTION public.update_af_user_deleted_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Check if 'deleted_at' field is modified
|
||||
IF OLD.deleted_at IS DISTINCT FROM NEW.deleted_at THEN
|
||||
-- Update 'deleted_at' in public.af_user
|
||||
UPDATE public.af_user
|
||||
SET deleted_at = NEW.deleted_at
|
||||
WHERE uuid = NEW.id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_af_user_deleted_at_trigger
|
||||
AFTER UPDATE OF deleted_at ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_af_user_deleted_at();
|
||||
|
|
@ -26,7 +26,7 @@ DO $$ BEGIN IF NOT EXISTS (
|
|||
SELECT
|
||||
FROM pg_catalog.pg_roles
|
||||
WHERE rolname = 'supabase_auth_admin'
|
||||
) THEN CREATE USER supabase_auth_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD 'root';
|
||||
) THEN CREATE USER supabase_auth_admin BYPASSRLS NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD 'root';
|
||||
END IF;
|
||||
END $$;
|
||||
-- Create auth schema if it does not exist
|
||||
|
|
@ -34,5 +34,4 @@ CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin;
|
|||
-- Grant permissions
|
||||
GRANT CREATE ON DATABASE postgres TO supabase_auth_admin;
|
||||
-- Set search_path for supabase_auth_admin
|
||||
ALTER USER supabase_auth_admin
|
||||
SET search_path = 'auth';
|
||||
ALTER USER supabase_auth_admin SET search_path = 'auth';
|
||||
|
|
|
|||
|
|
@ -241,14 +241,14 @@ async fn setup_admin_account(
|
|||
},
|
||||
_ => {
|
||||
let user_id = admin_user.id.parse::<uuid::Uuid>()?;
|
||||
let result = sqlx::query(
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
SET role = 'supabase_admin', email_confirmed_at = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
user_id,
|
||||
)
|
||||
.bind(user_id)
|
||||
.execute(pg_pool)
|
||||
.await
|
||||
.context("failed to update the admin user")?;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
use gotrue::params::{AdminDeleteUserParams, AdminUserParams};
|
||||
|
||||
use crate::{
|
||||
localhost_client,
|
||||
user::utils::{
|
||||
admin_user_client, generate_unique_registered_user_client, localhost_gotrue_client,
|
||||
},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_delete_create_same_user_hard() {
|
||||
let (client, user) = generate_unique_registered_user_client().await;
|
||||
let workspaces = client.get_workspaces().await.unwrap().0;
|
||||
assert_eq!(workspaces.len(), 1);
|
||||
let workspace_id = workspaces[0].workspace_id;
|
||||
let user_uuid = client.get_profile().await.unwrap().uuid;
|
||||
|
||||
let admin_token = {
|
||||
let admin_client = admin_user_client().await;
|
||||
admin_client.access_token().unwrap()
|
||||
};
|
||||
|
||||
// Delete user as admin
|
||||
let gotrue_client: gotrue::api::Client = localhost_gotrue_client();
|
||||
gotrue_client
|
||||
.admin_delete_user(
|
||||
&admin_token,
|
||||
&user_uuid.to_string(),
|
||||
&AdminDeleteUserParams {
|
||||
should_soft_delete: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Recreate same user
|
||||
gotrue_client
|
||||
.admin_add_user(
|
||||
&admin_token,
|
||||
&AdminUserParams {
|
||||
email: user.email.clone(),
|
||||
password: Some(user.password.clone()),
|
||||
email_confirm: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Login with recreated user
|
||||
let client = localhost_client();
|
||||
client
|
||||
.sign_in_password(&user.email, &user.password)
|
||||
.await
|
||||
.unwrap();
|
||||
let recreated_user_uuid = client.get_profile().await.unwrap().uuid;
|
||||
let recreated_workspace_uuid = client.get_workspaces().await.unwrap().0[0].workspace_id;
|
||||
assert_ne!(user_uuid, recreated_user_uuid);
|
||||
assert_ne!(workspace_id, recreated_workspace_uuid);
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
mod delete;
|
||||
mod refresh;
|
||||
mod sign_in;
|
||||
mod sign_out;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ use sqlx::types::Uuid;
|
|||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::localhost_client;
|
||||
use crate::util::setup_log;
|
||||
use crate::{localhost_client, LOCALHOST_GOTRUE};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ADMIN_USER: User = {
|
||||
|
|
@ -73,3 +73,8 @@ pub async fn generate_sign_in_action_link(email: &str) -> String {
|
|||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn localhost_gotrue_client() -> gotrue::api::Client {
|
||||
let reqwest_client = reqwest::Client::new();
|
||||
gotrue::api::Client::new(reqwest_client, LOCALHOST_GOTRUE)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue