mirror of
https://github.com/penpot/penpot.git
synced 2025-06-04 07:11:38 +02:00
🎉 Add manage cli helper.
This commit is contained in:
parent
82d7a0163d
commit
65a3126f15
6 changed files with 306 additions and 55 deletions
|
@ -7,6 +7,7 @@
|
||||||
org.clojure/clojurescript {:mvn/version "1.10.773"}
|
org.clojure/clojurescript {:mvn/version "1.10.773"}
|
||||||
org.clojure/data.json {:mvn/version "1.0.0"}
|
org.clojure/data.json {:mvn/version "1.0.0"}
|
||||||
org.clojure/core.async {:mvn/version "1.3.610"}
|
org.clojure/core.async {:mvn/version "1.3.610"}
|
||||||
|
org.clojure/tools.cli {:mvn/version "1.0.194"}
|
||||||
|
|
||||||
;; Logging
|
;; Logging
|
||||||
org.clojure/tools.logging {:mvn/version "1.1.0"}
|
org.clojure/tools.logging {:mvn/version "1.1.0"}
|
||||||
|
|
|
@ -25,12 +25,8 @@ echo $NEWCP > ./target/dist/classpath;
|
||||||
|
|
||||||
tee -a ./target/dist/run.sh >> /dev/null <<EOF
|
tee -a ./target/dist/run.sh >> /dev/null <<EOF
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
CP="$NEWCP"
|
CP="$NEWCP"
|
||||||
|
|
||||||
# Exports
|
|
||||||
|
|
||||||
# Find java executable
|
|
||||||
set +e
|
set +e
|
||||||
JAVA_CMD=\$(type -p java)
|
JAVA_CMD=\$(type -p java)
|
||||||
|
|
||||||
|
@ -52,4 +48,30 @@ set -x
|
||||||
exec \$JAVA_CMD \$JVM_OPTS -classpath \$CP -Dlog4j.configurationFile=./log4j2.xml "\$@" clojure.main -m app.main
|
exec \$JAVA_CMD \$JVM_OPTS -classpath \$CP -Dlog4j.configurationFile=./log4j2.xml "\$@" clojure.main -m app.main
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
tee -a ./target/dist/manage.sh >> /dev/null <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
CP="$NEWCP"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
JAVA_CMD=\$(type -p java)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
if [[ ! -n "\$JAVA_CMD" ]]; then
|
||||||
|
if [[ -n "\$JAVA_HOME" ]] && [[ -x "\$JAVA_HOME/bin/java" ]]; then
|
||||||
|
JAVA_CMD="\$JAVA_HOME/bin/java"
|
||||||
|
else
|
||||||
|
>&2 echo "Couldn't find 'java'. Please set JAVA_HOME."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f ./environ ]; then
|
||||||
|
source ./environ
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec \$JAVA_CMD \$JVM_OPTS -classpath \$CP -Dlog4j.configurationFile=./log4j2.xml clojure.main -m app.cli.manage "\$@"
|
||||||
|
EOF
|
||||||
|
|
||||||
chmod +x ./target/dist/run.sh
|
chmod +x ./target/dist/run.sh
|
||||||
|
chmod +x ./target/dist/manage.sh
|
||||||
|
|
||||||
|
|
172
backend/src/app/cli/manage.clj
Normal file
172
backend/src/app/cli/manage.clj
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2021 UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.cli.manage
|
||||||
|
"A manage cli api."
|
||||||
|
(:require
|
||||||
|
[app.config :as cfg]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.main :as main]
|
||||||
|
[app.rpc.mutations.profile :as profile]
|
||||||
|
[app.rpc.queries.profile :refer [retrieve-profile-data-by-email]]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[clojure.tools.cli :refer [parse-opts]]
|
||||||
|
[clojure.tools.logging :as log]
|
||||||
|
[integrant.core :as ig])
|
||||||
|
(:import
|
||||||
|
java.io.Console))
|
||||||
|
|
||||||
|
;; --- IMPL
|
||||||
|
|
||||||
|
(defn init-system
|
||||||
|
[]
|
||||||
|
(let [data (-> (main/build-system-config cfg/config)
|
||||||
|
(select-keys [:app.db/pool :app.metrics/metrics])
|
||||||
|
(assoc :app.migrations/all {}))]
|
||||||
|
(-> data ig/prep ig/init)))
|
||||||
|
|
||||||
|
(defn- read-from-console
|
||||||
|
[{:keys [label type] :or {type :text}}]
|
||||||
|
(let [^Console console (System/console)]
|
||||||
|
(when-not console
|
||||||
|
(log/error "no console found, can proceed")
|
||||||
|
(System/exit 1))
|
||||||
|
|
||||||
|
(binding [*out* (.writer console)]
|
||||||
|
(print label " ")
|
||||||
|
(.flush *out*))
|
||||||
|
|
||||||
|
(case type
|
||||||
|
:text (.readLine console)
|
||||||
|
:password (String. (.readPassword console)))))
|
||||||
|
|
||||||
|
(defn create-profile
|
||||||
|
[options]
|
||||||
|
(let [system (init-system)
|
||||||
|
email (or (:email options)
|
||||||
|
(read-from-console {:label "Email:"}))
|
||||||
|
fullname (or (:fullname options)
|
||||||
|
(read-from-console {:label "Full Name:"}))
|
||||||
|
password (or (:password options)
|
||||||
|
(read-from-console {:label "Password:"
|
||||||
|
:type :password}))]
|
||||||
|
(try
|
||||||
|
(db/with-atomic [conn (:app.db/pool system)]
|
||||||
|
(->> (profile/create-profile conn
|
||||||
|
{:fullname fullname
|
||||||
|
:email email
|
||||||
|
:password password
|
||||||
|
:is-active true
|
||||||
|
:is-demo false})
|
||||||
|
(profile/create-profile-relations conn)))
|
||||||
|
|
||||||
|
(when (pos? (:verbosity options))
|
||||||
|
(println "User created successfully."))
|
||||||
|
(System/exit 0)
|
||||||
|
|
||||||
|
(catch Exception _e
|
||||||
|
(when (pos? (:verbosity options))
|
||||||
|
(println "Unable to create user, already exists."))
|
||||||
|
(System/exit 1)))))
|
||||||
|
|
||||||
|
(defn reset-password
|
||||||
|
[options]
|
||||||
|
(let [system (init-system)]
|
||||||
|
(try
|
||||||
|
(db/with-atomic [conn (:app.db/pool system)]
|
||||||
|
(let [email (or (:email options)
|
||||||
|
(read-from-console {:label "Email:"}))
|
||||||
|
profile (retrieve-profile-data-by-email conn email)]
|
||||||
|
(when-not profile
|
||||||
|
(when (pos? (:verbosity options))
|
||||||
|
(println "Profile does not exists."))
|
||||||
|
(System/exit 1))
|
||||||
|
|
||||||
|
(let [password (or (:password options)
|
||||||
|
(read-from-console {:label "Password:"
|
||||||
|
:type :password}))]
|
||||||
|
(profile/update-profile-password! conn (assoc profile :password password))
|
||||||
|
(when (pos? (:verbosity options))
|
||||||
|
(println "Password changed successfully.")))))
|
||||||
|
(System/exit 0)
|
||||||
|
(catch Exception e
|
||||||
|
(when (pos? (:verbosity options))
|
||||||
|
(println "Unable to change password."))
|
||||||
|
(when (= 2 (:verbosity options))
|
||||||
|
(.printStackTrace e))
|
||||||
|
(System/exit 1)))))
|
||||||
|
|
||||||
|
;; --- CLI PARSE
|
||||||
|
|
||||||
|
(def cli-options
|
||||||
|
;; An option with a required argument
|
||||||
|
[["-u" "--email EMAIL" "Email Address"]
|
||||||
|
["-p" "--password PASSWORD" "Password"]
|
||||||
|
["-n" "--name FULLNAME" "Full Name"]
|
||||||
|
["-v" nil "Verbosity level"
|
||||||
|
:id :verbosity
|
||||||
|
:default 1
|
||||||
|
:update-fn inc]
|
||||||
|
["-q" nil "Dont' print to console"
|
||||||
|
:id :verbosity
|
||||||
|
:update-fn (constantly 0)]
|
||||||
|
["-h" "--help"]])
|
||||||
|
|
||||||
|
(defn usage
|
||||||
|
[options-summary]
|
||||||
|
(->> ["Penpot CLI management."
|
||||||
|
""
|
||||||
|
"Usage: manage [options] action"
|
||||||
|
""
|
||||||
|
"Options:"
|
||||||
|
options-summary
|
||||||
|
""
|
||||||
|
"Actions:"
|
||||||
|
" create-profile Create new profile."
|
||||||
|
" reset-password Reset profile password."
|
||||||
|
""]
|
||||||
|
(str/join \newline)))
|
||||||
|
|
||||||
|
(defn error-msg [errors]
|
||||||
|
(str "The following errors occurred while parsing your command:\n\n"
|
||||||
|
(str/join \newline errors)))
|
||||||
|
|
||||||
|
(defn validate-args
|
||||||
|
"Validate command line arguments. Either return a map indicating the program
|
||||||
|
should exit (with a error message, and optional ok status), or a map
|
||||||
|
indicating the action the program should take and the options provided."
|
||||||
|
[args]
|
||||||
|
(let [{:keys [options arguments errors summary] :as opts} (parse-opts args cli-options)]
|
||||||
|
;; (pp/pprint opts)
|
||||||
|
(cond
|
||||||
|
(:help options) ; help => exit OK with usage summary
|
||||||
|
{:exit-message (usage summary) :ok? true}
|
||||||
|
|
||||||
|
errors ; errors => exit with description of errors
|
||||||
|
{:exit-message (error-msg errors)}
|
||||||
|
|
||||||
|
;; custom validation on arguments
|
||||||
|
:else
|
||||||
|
(let [action (first arguments)]
|
||||||
|
(if (#{"create-profile" "reset-password"} action)
|
||||||
|
{:action (first arguments) :options options}
|
||||||
|
{:exit-message (usage summary)})))))
|
||||||
|
|
||||||
|
(defn exit [status msg]
|
||||||
|
(println msg)
|
||||||
|
(System/exit status))
|
||||||
|
|
||||||
|
(defn -main
|
||||||
|
[& args]
|
||||||
|
(let [{:keys [action options exit-message ok?]} (validate-args args)]
|
||||||
|
(if exit-message
|
||||||
|
(exit (if ok? 0 1) exit-message)
|
||||||
|
(case action
|
||||||
|
"create-profile" (create-profile options)
|
||||||
|
"reset-password" (reset-password options)))))
|
|
@ -341,12 +341,8 @@
|
||||||
|
|
||||||
;; --- Mutation: Update Password
|
;; --- Mutation: Update Password
|
||||||
|
|
||||||
(defn- validate-password!
|
(declare validate-password!)
|
||||||
[conn {:keys [profile-id old-password] :as params}]
|
(declare update-profile-password!)
|
||||||
(let [profile (db/get-by-id conn :profile profile-id)]
|
|
||||||
(when-not (:valid (verify-password old-password (:password profile)))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :old-password-not-match))))
|
|
||||||
|
|
||||||
(s/def ::update-profile-password
|
(s/def ::update-profile-password
|
||||||
(s/keys :req-un [::profile-id ::password ::old-password]))
|
(s/keys :req-un [::profile-id ::password ::old-password]))
|
||||||
|
@ -354,12 +350,23 @@
|
||||||
(sv/defmethod ::update-profile-password {:rlimit :password}
|
(sv/defmethod ::update-profile-password {:rlimit :password}
|
||||||
[{:keys [pool] :as cfg} {:keys [password profile-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [password profile-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(validate-password! conn params)
|
(let [profile (validate-password! conn params)]
|
||||||
|
(update-profile-password! conn (assoc profile :password password))
|
||||||
|
nil)))
|
||||||
|
|
||||||
|
(defn- validate-password!
|
||||||
|
[conn {:keys [profile-id old-password] :as params}]
|
||||||
|
(let [profile (db/get-by-id conn :profile profile-id)]
|
||||||
|
(when-not (:valid (verify-password old-password (:password profile)))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :old-password-not-match))
|
||||||
|
profile))
|
||||||
|
|
||||||
|
(defn update-profile-password!
|
||||||
|
[conn {:keys [id password] :as profile}]
|
||||||
(db/update! conn :profile
|
(db/update! conn :profile
|
||||||
{:password (derive-password password)}
|
{:password (derive-password password)}
|
||||||
{:id profile-id})
|
{:id id}))
|
||||||
nil))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Update Photo
|
;; --- Mutation: Update Photo
|
||||||
|
|
||||||
|
@ -393,20 +400,42 @@
|
||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Request Email Change
|
;; --- Mutation: Request Email Change
|
||||||
|
|
||||||
|
(declare request-email-change)
|
||||||
|
(declare change-email-inmediatelly)
|
||||||
|
|
||||||
(s/def ::request-email-change
|
(s/def ::request-email-change
|
||||||
(s/keys :req-un [::email]))
|
(s/keys :req-un [::email]))
|
||||||
|
|
||||||
(sv/defmethod ::request-email-change
|
(sv/defmethod ::request-email-change
|
||||||
[{:keys [pool tokens] :as cfg} {:keys [profile-id email] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id email] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [email (str/lower email)
|
(let [profile (db/get-by-id conn :profile profile-id)
|
||||||
profile (db/get-by-id conn :profile profile-id)
|
cfg (assoc cfg :conn conn)
|
||||||
token (tokens :generate
|
params (assoc params
|
||||||
|
:profile profile
|
||||||
|
:email (str/lower email))]
|
||||||
|
(if (cfg/get :smtp-enabled)
|
||||||
|
(request-email-change cfg params)
|
||||||
|
(change-email-inmediatelly cfg params)))))
|
||||||
|
|
||||||
|
(defn- change-email-inmediatelly
|
||||||
|
[{:keys [conn]} {:keys [profile email] :as params}]
|
||||||
|
(when (not= email (:email profile))
|
||||||
|
(check-profile-existence! conn params))
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:email email}
|
||||||
|
{:id (:id profile)})
|
||||||
|
{:changed true})
|
||||||
|
|
||||||
|
(defn- request-email-change
|
||||||
|
[{:keys [conn tokens]} {:keys [profile email] :as params}]
|
||||||
|
(let [token (tokens :generate
|
||||||
{:iss :change-email
|
{:iss :change-email
|
||||||
:exp (dt/in-future "15m")
|
:exp (dt/in-future "15m")
|
||||||
:profile-id profile-id
|
:profile-id (:id profile)
|
||||||
:email email})
|
:email email})
|
||||||
ptoken (tokens :generate-predefined
|
ptoken (tokens :generate-predefined
|
||||||
{:iss :profile-identity
|
{:iss :profile-identity
|
||||||
|
@ -431,7 +460,8 @@
|
||||||
:pending-email email
|
:pending-email email
|
||||||
:token token
|
:token token
|
||||||
:extra-data ptoken})
|
:extra-data ptoken})
|
||||||
nil)))
|
nil))
|
||||||
|
|
||||||
|
|
||||||
(defn select-profile-for-update
|
(defn select-profile-for-update
|
||||||
[conn id]
|
[conn id]
|
||||||
|
|
|
@ -281,7 +281,10 @@
|
||||||
(t/is (= (:email data) (:email result)))))))
|
(t/is (= (:email data) (:email result)))))))
|
||||||
|
|
||||||
(t/deftest test-email-change-request
|
(t/deftest test-email-change-request
|
||||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}
|
||||||
|
cfg-get-mock {:target 'app.config/get
|
||||||
|
:return (th/mock-config-get-with
|
||||||
|
{:smtp-enabled true})}]
|
||||||
(let [profile (th/create-profile* 1)
|
(let [profile (th/create-profile* 1)
|
||||||
pool (:app.db/pool th/*system*)
|
pool (:app.db/pool th/*system*)
|
||||||
data {::th/type :request-email-change
|
data {::th/type :request-email-change
|
||||||
|
@ -292,7 +295,7 @@
|
||||||
(let [out (th/mutation! data)]
|
(let [out (th/mutation! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:result out)))
|
(t/is (nil? (:result out)))
|
||||||
(let [mock (deref mock)]
|
(let [mock (deref email-send-mock)]
|
||||||
(t/is (= 1 (:call-count mock)))
|
(t/is (= 1 (:call-count mock)))
|
||||||
(t/is (true? (:called? mock)))))
|
(t/is (true? (:called? mock)))))
|
||||||
|
|
||||||
|
@ -301,7 +304,7 @@
|
||||||
(let [out (th/mutation! data)]
|
(let [out (th/mutation! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:result out)))
|
(t/is (nil? (:result out)))
|
||||||
(t/is (= 2 (:call-count (deref mock)))))
|
(t/is (= 2 (:call-count (deref email-send-mock)))))
|
||||||
|
|
||||||
;; with bounces
|
;; with bounces
|
||||||
(th/create-global-complaint-for pool {:type :bounce :email (:email data)})
|
(th/create-global-complaint-for pool {:type :bounce :email (:email data)})
|
||||||
|
@ -311,7 +314,27 @@
|
||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-info? error))
|
||||||
(t/is (th/ex-of-type? error :validation))
|
(t/is (th/ex-of-type? error :validation))
|
||||||
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
|
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
|
||||||
(t/is (= 2 (:call-count (deref mock))))))))
|
(t/is (= 2 (:call-count (deref email-send-mock))))))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest test-email-change-request-without-smtp
|
||||||
|
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}
|
||||||
|
cfg-get-mock {:target 'app.config/get
|
||||||
|
:return (th/mock-config-get-with
|
||||||
|
{:smtp-enabled false})}]
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
pool (:app.db/pool th/*system*)
|
||||||
|
data {::th/type :request-email-change
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:email "user1@example.com"}]
|
||||||
|
|
||||||
|
;; without complaints
|
||||||
|
(let [out (th/mutation! data)
|
||||||
|
res (:result out)]
|
||||||
|
(t/is (= {:changed true} res))
|
||||||
|
(let [mock (deref email-send-mock)]
|
||||||
|
(t/is (false? (:called? mock))))))))
|
||||||
|
|
||||||
|
|
||||||
(t/deftest test-request-profile-recovery
|
(t/deftest test-request-profile-recovery
|
||||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||||
|
|
|
@ -58,10 +58,13 @@
|
||||||
|
|
||||||
(defn- on-success
|
(defn- on-success
|
||||||
[form data]
|
[form data]
|
||||||
|
(if (:changed data)
|
||||||
|
(st/emit! (du/fetch-profile)
|
||||||
|
(modal/hide))
|
||||||
(let [email (get-in @form [:clean-data :email-1])
|
(let [email (get-in @form [:clean-data :email-1])
|
||||||
message (tr "notifications.validation-email-sent" email)]
|
message (tr "notifications.validation-email-sent" email)]
|
||||||
(st/emit! (dm/info message)
|
(st/emit! (dm/info message)
|
||||||
(modal/hide))))
|
(modal/hide)))))
|
||||||
|
|
||||||
(defn- on-submit
|
(defn- on-submit
|
||||||
[form event]
|
[form event]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue