From cc5a58b6d1af7a9fbd8cecfb19ce2819031e0037 Mon Sep 17 00:00:00 2001 From: ragilap Date: Fri, 2 Jan 2026 12:04:50 +0700 Subject: [PATCH] feat(BE): sso delete and fix response too many request --- ...02045853_fix_soft_delete_fk_casts.down.sql | 126 ++++++++++++++++ ...0102045853_fix_soft_delete_fk_casts.up.sql | 142 ++++++++++++++++++ .../modules/sso/controllers/sso.controller.go | 3 + .../users/repositories/user.repository.go | 4 +- 4 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 internal/database/migrations/20260102045853_fix_soft_delete_fk_casts.down.sql create mode 100644 internal/database/migrations/20260102045853_fix_soft_delete_fk_casts.up.sql diff --git a/internal/database/migrations/20260102045853_fix_soft_delete_fk_casts.down.sql b/internal/database/migrations/20260102045853_fix_soft_delete_fk_casts.down.sql new file mode 100644 index 00000000..161d3d89 --- /dev/null +++ b/internal/database/migrations/20260102045853_fix_soft_delete_fk_casts.down.sql @@ -0,0 +1,126 @@ +CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$ +DECLARE + fk record; + child_column text; + parent_column text; + parent_value text; + child_has_deleted_at boolean; + ref_exists boolean; + sql text; +BEGIN + IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN + FOR fk IN + SELECT conrelid::regclass AS child_table, + conkey AS child_cols, + confkey AS parent_cols, + confdeltype + FROM pg_constraint + WHERE contype = 'f' + AND confrelid = TG_RELID + LOOP + IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1 + OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN + RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table; + CONTINUE; + END IF; + + SELECT attname INTO child_column + FROM pg_attribute + WHERE attrelid = fk.child_table + AND attnum = fk.child_cols[1] + AND NOT attisdropped; + + SELECT attname INTO parent_column + FROM pg_attribute + WHERE attrelid = TG_RELID + AND attnum = fk.parent_cols[1] + AND NOT attisdropped; + + EXECUTE format('SELECT ($1).%I', parent_column) + INTO parent_value + USING OLD; + + SELECT EXISTS ( + SELECT 1 + FROM pg_attribute + WHERE attrelid = fk.child_table + AND attname = 'deleted_at' + AND NOT attisdropped + ) INTO child_has_deleted_at; + + IF fk.confdeltype IN ('r', 'a') THEN + sql := format( + 'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)', + fk.child_table, + child_column, + CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END + ); + EXECUTE sql INTO ref_exists USING parent_value; + IF ref_exists THEN + RAISE EXCEPTION 'Cannot soft delete %, still referenced by %', + TG_TABLE_NAME, fk.child_table; + END IF; + ELSIF fk.confdeltype = 'n' THEN + sql := format( + 'UPDATE %s SET %I = NULL WHERE %I = $1 %s', + fk.child_table, + child_column, + child_column, + CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END + ); + EXECUTE sql USING parent_value; + ELSIF fk.confdeltype = 'c' THEN + IF child_has_deleted_at THEN + sql := format( + 'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL', + fk.child_table, + child_column + ); + EXECUTE sql USING parent_value; + ELSE + sql := format( + 'DELETE FROM %s WHERE %I = $1', + fk.child_table, + child_column + ); + EXECUTE sql USING parent_value; + END IF; + ELSIF fk.confdeltype = 'd' THEN + sql := format( + 'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s', + fk.child_table, + child_column, + child_column, + CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END + ); + EXECUTE sql USING parent_value; + END IF; + END LOOP; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DO $$ +DECLARE + r record; + trigger_name text; +BEGIN + FOR r IN + SELECT table_schema, table_name + FROM information_schema.columns + WHERE column_name = 'deleted_at' + AND table_schema = 'public' + GROUP BY table_schema, table_name + LOOP + trigger_name := format('trg_soft_delete_fk_%s', r.table_name); + EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name); + EXECUTE format( + 'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()', + trigger_name, + r.table_schema, + r.table_name + ); + END LOOP; +END $$; diff --git a/internal/database/migrations/20260102045853_fix_soft_delete_fk_casts.up.sql b/internal/database/migrations/20260102045853_fix_soft_delete_fk_casts.up.sql new file mode 100644 index 00000000..2801ac2e --- /dev/null +++ b/internal/database/migrations/20260102045853_fix_soft_delete_fk_casts.up.sql @@ -0,0 +1,142 @@ +CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$ +DECLARE + fk record; + child_column text; + parent_column text; + parent_value text; + child_has_deleted_at boolean; + ref_exists boolean; + sql text; + child_type text; +BEGIN + IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN + FOR fk IN + SELECT conrelid::regclass AS child_table, + conkey AS child_cols, + confkey AS parent_cols, + confdeltype + FROM pg_constraint + WHERE contype = 'f' + AND confrelid = TG_RELID + LOOP + IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1 + OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN + RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table; + CONTINUE; + END IF; + + SELECT attname INTO child_column + FROM pg_attribute + WHERE attrelid = fk.child_table + AND attnum = fk.child_cols[1] + AND NOT attisdropped; + + SELECT attname INTO parent_column + FROM pg_attribute + WHERE attrelid = TG_RELID + AND attnum = fk.parent_cols[1] + AND NOT attisdropped; + + SELECT format_type(atttypid, atttypmod) INTO child_type + FROM pg_attribute + WHERE attrelid = fk.child_table + AND attname = child_column + AND NOT attisdropped; + + IF child_type IS NULL THEN + child_type := 'text'; + END IF; + + EXECUTE format('SELECT ($1).%I', parent_column) + INTO parent_value + USING OLD; + + SELECT EXISTS ( + SELECT 1 + FROM pg_attribute + WHERE attrelid = fk.child_table + AND attname = 'deleted_at' + AND NOT attisdropped + ) INTO child_has_deleted_at; + + IF fk.confdeltype IN ('r', 'a') THEN + sql := format( + 'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1::%s %s)', + fk.child_table, + child_column, + child_type, + CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END + ); + EXECUTE sql INTO ref_exists USING parent_value; + IF ref_exists THEN + RAISE EXCEPTION 'Cannot soft delete %, still referenced by %', + TG_TABLE_NAME, fk.child_table; + END IF; + ELSIF fk.confdeltype = 'n' THEN + sql := format( + 'UPDATE %s SET %I = NULL WHERE %I = $1::%s %s', + fk.child_table, + child_column, + child_column, + child_type, + CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END + ); + EXECUTE sql USING parent_value; + ELSIF fk.confdeltype = 'c' THEN + IF child_has_deleted_at THEN + sql := format( + 'UPDATE %s SET deleted_at = NOW() WHERE %I = $1::%s AND deleted_at IS NULL', + fk.child_table, + child_column, + child_type + ); + EXECUTE sql USING parent_value; + ELSE + sql := format( + 'DELETE FROM %s WHERE %I = $1::%s', + fk.child_table, + child_column, + child_type + ); + EXECUTE sql USING parent_value; + END IF; + ELSIF fk.confdeltype = 'd' THEN + sql := format( + 'UPDATE %s SET %I = DEFAULT WHERE %I = $1::%s %s', + fk.child_table, + child_column, + child_column, + child_type, + CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END + ); + EXECUTE sql USING parent_value; + END IF; + END LOOP; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DO $$ +DECLARE + r record; + trigger_name text; +BEGIN + FOR r IN + SELECT table_schema, table_name + FROM information_schema.columns + WHERE column_name = 'deleted_at' + AND table_schema = 'public' + GROUP BY table_schema, table_name + LOOP + trigger_name := format('trg_soft_delete_fk_%s', r.table_name); + EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name); + EXECUTE format( + 'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()', + trigger_name, + r.table_schema, + r.table_name + ); + END LOOP; +END $$; diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index 99bd67d6..554b3388 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -171,6 +171,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error { if resp.StatusCode >= 400 { utils.Log.Warnf("token refresh response status %d", resp.StatusCode) + if resp.StatusCode == fiber.StatusTooManyRequests { + return fiber.NewError(fiber.StatusTooManyRequests, "Too many attempts, please slow down") + } return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated") } diff --git a/internal/modules/users/repositories/user.repository.go b/internal/modules/users/repositories/user.repository.go index f9bee9ed..b3cac2dc 100644 --- a/internal/modules/users/repositories/user.repository.go +++ b/internal/modules/users/repositories/user.repository.go @@ -42,7 +42,7 @@ func (r *UserRepositoryImpl) GetByIdUser( modifier func(*gorm.DB) *gorm.DB, ) (*entity.User, error) { return r.BaseRepositoryImpl.First(ctx, func(db *gorm.DB) *gorm.DB { - return db.Where("id_user = ?", idUser) + return db.Where("id_user::bigint = ?::bigint", idUser) }) } @@ -93,7 +93,7 @@ func (r *UserRepositoryImpl) UpsertByIdUser(ctx context.Context, user *entity.Us } func (r *UserRepositoryImpl) SoftDeleteByIdUser(ctx context.Context, idUser int64) error { - query := r.DB().WithContext(ctx).Where("id_user = ?", idUser) + query := r.DB().WithContext(ctx).Where("id_user::bigint = ?::bigint", idUser) result := query.Delete(&entity.User{}) if result.Error != nil { return result.Error