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,
|
&session.token.access_token,
|
||||||
&user_uuid,
|
&user_uuid,
|
||||||
&AdminDeleteUserParams {
|
&AdminDeleteUserParams {
|
||||||
should_soft_delete: true,
|
should_soft_delete: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::notify::{ClientToken, TokenStateReceiver};
|
use crate::notify::{ClientToken, TokenStateReceiver};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use gotrue_entity::dto::AuthProvider;
|
||||||
use prost::Message as ProstMessage;
|
use prost::Message as ProstMessage;
|
||||||
|
|
||||||
use app_error::AppError;
|
use app_error::AppError;
|
||||||
|
|
@ -44,7 +45,7 @@ use url::Url;
|
||||||
use crate::retry::{RefreshTokenAction, RefreshTokenRetryCondition};
|
use crate::retry::{RefreshTokenAction, RefreshTokenRetryCondition};
|
||||||
use crate::ws::{WSClientHttpSender, WSError};
|
use crate::ws::{WSClientHttpSender, WSError};
|
||||||
use gotrue_entity::dto::SignUpResponse::{Authenticated, NotAuthenticated};
|
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;
|
use realtime_entity::realtime_proto::HttpRealtimeMessage;
|
||||||
|
|
||||||
/// `Client` is responsible for managing communication with the GoTrue API and cloud storage.
|
/// `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
|
SELECT
|
||||||
FROM pg_catalog.pg_roles
|
FROM pg_catalog.pg_roles
|
||||||
WHERE rolname = 'supabase_auth_admin'
|
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 IF;
|
||||||
END $$;
|
END $$;
|
||||||
-- Create auth schema if it does not exist
|
-- 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 permissions
|
||||||
GRANT CREATE ON DATABASE postgres TO supabase_auth_admin;
|
GRANT CREATE ON DATABASE postgres TO supabase_auth_admin;
|
||||||
-- Set search_path for supabase_auth_admin
|
-- Set search_path for supabase_auth_admin
|
||||||
ALTER USER supabase_auth_admin
|
ALTER USER supabase_auth_admin SET search_path = 'auth';
|
||||||
SET search_path = 'auth';
|
|
||||||
|
|
|
||||||
|
|
@ -241,14 +241,14 @@ async fn setup_admin_account(
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
let user_id = admin_user.id.parse::<uuid::Uuid>()?;
|
let user_id = admin_user.id.parse::<uuid::Uuid>()?;
|
||||||
let result = sqlx::query(
|
let result = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE auth.users
|
UPDATE auth.users
|
||||||
SET role = 'supabase_admin', email_confirmed_at = NOW()
|
SET role = 'supabase_admin', email_confirmed_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
"#,
|
"#,
|
||||||
|
user_id,
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
|
||||||
.execute(pg_pool)
|
.execute(pg_pool)
|
||||||
.await
|
.await
|
||||||
.context("failed to update the admin user")?;
|
.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 refresh;
|
||||||
mod sign_in;
|
mod sign_in;
|
||||||
mod sign_out;
|
mod sign_out;
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ use sqlx::types::Uuid;
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
use crate::localhost_client;
|
|
||||||
use crate::util::setup_log;
|
use crate::util::setup_log;
|
||||||
|
use crate::{localhost_client, LOCALHOST_GOTRUE};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref ADMIN_USER: User = {
|
pub static ref ADMIN_USER: User = {
|
||||||
|
|
@ -73,3 +73,8 @@ pub async fn generate_sign_in_action_link(email: &str) -> String {
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.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