diff --git a/backend/scripts/manage.py b/backend/scripts/manage.py index 56141571e7..f600b948b9 100755 --- a/backend/scripts/manage.py +++ b/backend/scripts/manage.py @@ -137,6 +137,23 @@ def search_profile(email): if isinstance(res, list): print(tabulate(res, headers="keys")) +def delete_profiles_in_bulk(path): + with open(path, encoding="utf-8") as email_file: + emails = [line.strip() for line in email_file if line.strip()] + + if not emails: + raise RuntimeError("the email file is empty") + + params = { + "cmd": "delete-profiles-in-bulk", + "params": { + "emails": emails, + } + } + + res = run_cmd(params) + print(f"Enqueued: {res['deleted']} / {res['total']}") + def derive_password(password): params = { "cmd": "derive-password", @@ -161,6 +178,7 @@ available_commands = ( "create-profile", "update-profile", "delete-profile", + "delete-profiles-in-bulk", "search-profile", "derive-password", ) @@ -178,6 +196,7 @@ parser.add_argument("-n", "--fullname", help="fullname", action="store") parser.add_argument("-e", "--email", help="email", action="store") parser.add_argument("-p", "--password", help="password", action="store") parser.add_argument("-c", "--connect", help="connect to PREPL", action="store", default="tcp://localhost:6063") +parser.add_argument("--file", help="file containing one email per line", action="store") parser.add_argument("--skip-tutorial", help="mark tutorial as viewed", action="store_true") parser.add_argument("--skip-walkthrough", help="mark walkthrough as viewed", action="store_true") @@ -237,4 +256,9 @@ elif args.action == "search-profile": search_profile(email) +elif args.action == "delete-profiles-in-bulk": + if args.file is None: + parser.error("delete-profiles-in-bulk requires --file") + + delete_profiles_in_bulk(args.file) diff --git a/backend/src/app/srepl/cli.clj b/backend/src/app/srepl/cli.clj index c47043cbad..3152714a70 100644 --- a/backend/src/app/srepl/cli.clj +++ b/backend/src/app/srepl/cli.clj @@ -13,11 +13,14 @@ [app.common.schema.generators :as sg] [app.common.time :as ct] [app.common.uuid :as uuid] + [app.config :as cf] [app.db :as db] + [app.loggers.audit :as audit] [app.rpc.commands.auth :as cmd.auth] [app.rpc.commands.profile :as cmd.profile] [app.setup :as-alias setup] [app.tokens :as tokens] + [app.worker :as wrk] [cuerdas.core :as str])) (defn coercer @@ -122,6 +125,50 @@ " where email similar to ? order by created_at desc limit 100")] (db/exec! conn [sql email])))))) +(defn delete-profiles! + "Enqueue the standard profile deletion task for each provided email." + ([system emails] + (delete-profiles! system emails + {:triggered-by "prepl" + :cause "explicit call to delete-profiles-in-bulk"})) + ([system emails context] + (let [deleted-at (ct/minus (ct/now) (cf/get-deletion-delay))] + (db/tx-run! system + (fn [system] + (reduce + (fn [{:keys [deleted total]} email] + (if-let [profile (some-> (db/get* system :profile + {:email (str/lower email)} + {::db/remove-deleted false}) + (cmd.profile/decode-row))] + (do + (audit/insert system + {:name "delete-profile" + :type "action" + :profile-id (:id profile) + :tracked-at deleted-at + :props (audit/profile->props profile) + :context context}) + (wrk/invoke! (-> system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :profile + :deleted-at deleted-at + :id (:id profile)}))) + {:deleted (inc deleted) :total (inc total)}) + {:deleted deleted :total (inc total)})) + {:deleted 0 :total 0} + emails)))))) + +(defmethod exec-command "delete-profiles-in-bulk" + [{:keys [emails]}] + (when-not (and (vector? emails) (seq emails) (every? string? emails)) + (ex/raise :type :assertion + :code :invalid-arguments + :hint "a non-empty email list should be provided")) + + (some-> (get-current-system) + (delete-profiles! emails))) + (defmethod exec-command "derive-password" [{:keys [password]}] (derive-password password)) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index fdc0caea1a..14620e1573 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -34,6 +34,7 @@ [app.rpc.commands.profile :as profile] [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] + [app.srepl.cli :as cli] [app.srepl.helpers :as h] [app.srepl.procs.file-repair :as procs.file-repair] [app.system :as sys] @@ -745,47 +746,15 @@ (defn delete-profiles-in-bulk! [system path] - (letfn [(process-data! [system deleted-at emails] - (loop [emails emails - deleted 0 - total 0] - (if-let [email (first emails)] - (if-let [profile (some-> (db/get* system :profile - {:email (str/lower email)} - {::db/remove-deleted false}) - (profile/decode-row))] - (do - (audit/insert system - {:name "delete-profile" - :type "action" - :profile-id (:id profile) - :tracked-at deleted-at - :props (audit/profile->props profile) - :context {:triggered-by "srepl" - :cause "explicit call to delete-profiles-in-bulk!"}}) - (wrk/invoke! (-> system - (assoc ::wrk/task :delete-object) - (assoc ::wrk/params {:object :profile - :deleted-at deleted-at - :id (:id profile)}))) - (recur (rest emails) - (inc deleted) - (inc total))) - (recur (rest emails) - deleted - (inc total))) - {:deleted deleted :total total})))] + (let [path (fs/path path)] + (when-not (fs/exists? path) + (throw (ex-info "path does not exists" {:path path}))) - (let [path (fs/path path) - deleted-at (ct/minus (ct/now) (cf/get-deletion-delay))] - - (when-not (fs/exists? path) - (throw (ex-info "path does not exists" {:path path}))) - - (db/tx-run! system - (fn [system] - (with-open [reader (io/reader path)] - (process-data! system deleted-at (line-seq reader)))))))) + (with-open [reader (io/reader path)] + (cli/delete-profiles! system + (line-seq reader) + {:triggered-by "srepl" + :cause "explicit call to delete-profiles-in-bulk!"})))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; CASCADE FIXING diff --git a/backend/test/backend_tests/srepl_cli_test.clj b/backend/test/backend_tests/srepl_cli_test.clj new file mode 100644 index 0000000000..e73eb5ae08 --- /dev/null +++ b/backend/test/backend_tests/srepl_cli_test.clj @@ -0,0 +1,32 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en EspaƱa SL + +(ns backend-tests.srepl-cli-test + (:require + [app.srepl.cli :as cli] + [app.system :as system] + [clojure.test :as t])) + +(t/deftest delete-profiles-in-bulk-command + (let [received (atom nil)] + (with-redefs [system/system {:service :test} + cli/delete-profiles! (fn [system emails] + (reset! received [system emails]) + {:deleted 2 :total 2})] + (t/is (= {:deleted 2 :total 2} + (cli/exec {:cmd "delete-profiles-in-bulk" + :params {:emails ["one@example.com" + "two@example.com"]}}))) + (t/is (= [{:service :test} + ["one@example.com" "two@example.com"]] + @received))))) + +(t/deftest delete-profiles-in-bulk-requires-emails + (t/is (thrown-with-msg? + clojure.lang.ExceptionInfo + #"non-empty email list" + (cli/exec {:cmd "delete-profiles-in-bulk" + :params {:emails []}}))))