mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 22:10:34 +00:00
✨ Add bulk profile deletion CLI
Allow administrative tools to submit a list of profile emails through the structured PREPL interface instead of evaluating Clojure expressions. Reuse the existing audited deletion path and expose the command through manage.py for automated maintenance jobs. AI-assisted-by: gpt-5
This commit is contained in:
parent
7348620fcd
commit
290923f0ae
@ -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)
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
32
backend/test/backend_tests/srepl_cli_test.clj
Normal file
32
backend/test/backend_tests/srepl_cli_test.clj
Normal file
@ -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 []}}))))
|
||||
Loading…
x
Reference in New Issue
Block a user