🎉 Add manage cli helper.

This commit is contained in:
Andrey Antukh 2021-02-24 11:51:57 +01:00 committed by Andrés Moya
parent 82d7a0163d
commit 65a3126f15
6 changed files with 306 additions and 55 deletions

View 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)))))

View file

@ -341,12 +341,8 @@
;; --- Mutation: Update Password
(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))))
(declare validate-password!)
(declare update-profile-password!)
(s/def ::update-profile-password
(s/keys :req-un [::profile-id ::password ::old-password]))
@ -354,12 +350,23 @@
(sv/defmethod ::update-profile-password {:rlimit :password}
[{:keys [pool] :as cfg} {:keys [password profile-id] :as params}]
(db/with-atomic [conn pool]
(validate-password! conn params)
(db/update! conn :profile
{:password (derive-password password)}
{:id profile-id})
nil))
(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
{:password (derive-password password)}
{:id id}))
;; --- Mutation: Update Photo
@ -393,45 +400,68 @@
{:id profile-id})
nil)
;; --- Mutation: Request Email Change
(declare request-email-change)
(declare change-email-inmediatelly)
(s/def ::request-email-change
(s/keys :req-un [::email]))
(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]
(let [email (str/lower email)
profile (db/get-by-id conn :profile profile-id)
token (tokens :generate
{:iss :change-email
:exp (dt/in-future "15m")
:profile-id profile-id
:email email})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(let [profile (db/get-by-id conn :profile profile-id)
cfg (assoc cfg :conn conn)
params (assoc params
:profile profile
:email (str/lower email))]
(if (cfg/get :smtp-enabled)
(request-email-change cfg params)
(change-email-inmediatelly cfg params)))))
(when (not= email (:email profile))
(check-profile-existence! conn 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})
(when-not (emails/allow-send-emails? conn profile)
(ex/raise :type :validation
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(defn- request-email-change
[{:keys [conn tokens]} {:keys [profile email] :as params}]
(let [token (tokens :generate
{:iss :change-email
:exp (dt/in-future "15m")
:profile-id (:id profile)
:email email})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(when (emails/has-bounce-reports? conn email)
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(when (not= email (:email profile))
(check-profile-existence! conn params))
(when-not (emails/allow-send-emails? conn profile)
(ex/raise :type :validation
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(when (emails/has-bounce-reports? conn email)
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(emails/send! conn emails/change-email
{:to (:email profile)
:name (:fullname profile)
:pending-email email
:token token
:extra-data ptoken})
nil))
(emails/send! conn emails/change-email
{:to (:email profile)
:name (:fullname profile)
:pending-email email
:token token
:extra-data ptoken})
nil)))
(defn select-profile-for-update
[conn id]