0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-11 07:11:32 -05:00

🐛 Check complaints reports in the same way as bounces are checked

This commit is contained in:
Andrey Antukh 2024-07-29 16:17:11 +02:00
parent e0f2c4e0aa
commit 52425a993a
12 changed files with 272 additions and 174 deletions

View file

@ -448,3 +448,11 @@
{:email email :type "bounce"} {:email email :type "bounce"}
{:limit 10}))] {:limit 10}))]
(>= (count reports) threshold)))) (>= (count reports) threshold))))
(defn has-reports?
([conn email] (has-reports? conn email nil))
([conn email {:keys [threshold] :or {threshold 1}}]
(let [reports (db/exec! conn (sql/select :global-complaint-report
{:email email}
{:limit 10}))]
(>= (count reports) threshold))))

View file

@ -9,6 +9,7 @@
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.pprint :as pp]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.http.client :as http] [app.http.client :as http]
@ -16,10 +17,10 @@
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.worker :as-alias wrk] [app.worker :as-alias wrk]
[clojure.data.json :as j]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[jsonista.core :as j]
[promesa.exec :as px] [promesa.exec :as px]
[ring.request :as rreq] [ring.request :as rreq]
[ring.response :as-alias rres])) [ring.response :as-alias rres]))
@ -136,83 +137,110 @@
(defn- parse-json (defn- parse-json
[v] [v]
(ex/ignoring (try
(j/read-value v))) (j/read-str v)
(catch Throwable cause
(l/wrn :hint "unable to decode request body"
:cause cause))))
(defn- register-bounce-for-profile (defn- register-bounce-for-profile
[{:keys [::db/pool]} {:keys [type kind profile-id] :as report}] [{:keys [::db/pool]} {:keys [type kind profile-id] :as report}]
(when (= kind "permanent") (when (= kind "permanent")
(db/with-atomic [conn pool] (try
(db/insert! conn :profile-complaint-report (db/insert! pool :profile-complaint-report
{:profile-id profile-id {:profile-id profile-id
:type (name type) :type (name type)
:content (db/tjson report)}) :content (db/tjson report)})
;; TODO: maybe also try to find profiles by mail and if exists (catch Throwable cause
;; register profile reports for them? (l/warn :hint "unable to persist profile complaint"
(doseq [recipient (:recipients report)] :cause cause)))
(db/insert! conn :global-complaint-report
{:email (:email recipient)
:type (name type)
:content (db/tjson report)}))
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))] (doseq [recipient (:recipients report)]
(when (some #(= (:email profile) (:email %)) (:recipients report)) (db/insert! pool :global-complaint-report
;; If the report matches the profile email, this means that {:email (:email recipient)
;; the report is for itself, can be caused when a user
;; registers with an invalid email or the user email is
;; permanently rejecting receiving the email. In this case we
;; have no option to mark the user as muted (and in this case
;; the profile will be also inactive.
(db/update! conn :profile
{:is-muted true}
{:id profile-id}))))))
(defn- register-complaint-for-profile
[{:keys [::db/pool]} {:keys [type profile-id] :as report}]
(db/with-atomic [conn pool]
(db/insert! conn :profile-complaint-report
{:profile-id profile-id
:type (name type)
:content (db/tjson report)})
;; TODO: maybe also try to find profiles by email and if exists
;; register profile reports for them?
(doseq [email (:recipients report)]
(db/insert! conn :global-complaint-report
{:email email
:type (name type) :type (name type)
:content (db/tjson report)})) :content (db/tjson report)}))
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))] (let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))]
(when (some #(= % (:email profile)) (:recipients report)) (when (some #(= (:email profile) (:email %)) (:recipients report))
;; If the report matches the profile email, this means that ;; If the report matches the profile email, this means that
;; the report is for itself, rare case but can happen; In this ;; the report is for itself, can be caused when a user
;; case just mark profile as muted (very rare case). ;; registers with an invalid email or the user email is
(db/update! conn :profile ;; permanently rejecting receiving the email. In this case we
;; have no option to mark the user as muted (and in this case
;; the profile will be also inactive.
(l/inf :hint "mark profile: muted"
:profile-id (str (:id profile))
:email (:email profile)
:reason "bounce report"
:report-id (:feedback-id report))
(db/update! pool :profile
{:is-muted true} {:is-muted true}
{:id profile-id}))))) {:id profile-id}
{::db/return-keys false})))))
(defn- register-complaint-for-profile
[{:keys [::db/pool]} {:keys [type profile-id] :as report}]
(try
(db/insert! pool :profile-complaint-report
{:profile-id profile-id
:type (name type)
:content (db/tjson report)})
(catch Throwable cause
(l/warn :hint "unable to persist profile complaint"
:cause cause)))
;; TODO: maybe also try to find profiles by email and if exists
;; register profile reports for them?
(doseq [email (:recipients report)]
(db/insert! pool :global-complaint-report
{:email email
:type (name type)
:content (db/tjson report)}))
(let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))]
(when (some #(= % (:email profile)) (:recipients report))
;; If the report matches the profile email, this means that
;; the report is for itself, rare case but can happen; In this
;; case just mark profile as muted (very rare case).
(l/inf :hint "mark profile: muted"
:profile-id (str (:id profile))
:email (:email profile)
:reason "complaint report"
:report-id (:feedback-id report))
(db/update! pool :profile
{:is-muted true}
{:id profile-id}
{::db/return-keys false}))))
(defn- process-report (defn- process-report
[cfg {:keys [type profile-id] :as report}] [cfg {:keys [type profile-id] :as report}]
(l/trace :action "processing report" :report (pr-str report))
(cond (cond
;; In this case we receive a bounce/complaint notification without ;; In this case we receive a bounce/complaint notification without
;; confirmed identity, we just emit a warning but do nothing about ;; confirmed identity, we just emit a warning but do nothing about
;; it because this is not a normal case. All notifications should ;; it because this is not a normal case. All notifications should
;; come with profile identity. ;; come with profile identity.
(nil? profile-id) (nil? profile-id)
(l/warn :msg "a notification without identity received from AWS" (l/wrn :hint "not-identified report"
:report (pr-str report)) ::l/body (pp/pprint-str report {:length 40 :level 6}))
(= "bounce" type) (= "bounce" type)
(register-bounce-for-profile cfg report) (do
(l/trc :hint "bounce report"
::l/body (pp/pprint-str report {:length 40 :level 6}))
(register-bounce-for-profile cfg report))
(= "complaint" type) (= "complaint" type)
(register-complaint-for-profile cfg report) (do
(l/trc :hint "complaint report"
::l/body (pp/pprint-str report {:length 40 :level 6}))
(register-complaint-for-profile cfg report))
:else :else
(l/warn :msg "unrecognized report received from AWS" (l/wrn :hint "unrecognized report"
:report (pr-str report)))) ::l/body (pp/pprint-str report {:length 20 :level 4}))))

View file

@ -209,7 +209,19 @@
(str/lower (:password params))) (str/lower (:password params)))
(ex/raise :type :validation (ex/raise :type :validation
:code :email-as-password :code :email-as-password
:hint "you can't use your email as password"))) :hint "you can't use your email as password"))
(when (eml/has-bounce-reports? cfg (:email params))
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email (:email params)
:hint "looks like the email has bounce reports"))
(when (eml/has-complaint-reports? cfg (:email params))
(ex/raise :type :restriction
:code :email-has-complaints
:email (:email params)
:hint "looks like the email has complaint reports")))
(defn prepare-register (defn prepare-register
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}] [{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
@ -398,20 +410,22 @@
::audit/profile-id (:id profile)})) ::audit/profile-id (:id profile)}))
(do (do
(send-email-verification! cfg profile) (when-not (eml/has-reports? conn (:email profile))
(send-email-verification! cfg profile))
(rph/with-meta {:email (:email profile)} (rph/with-meta {:email (:email profile)}
{::audit/replace-props props {::audit/replace-props props
::audit/context {:action "email-verification"} ::audit/context {:action "email-verification"}
::audit/profile-id (:id profile)}))) ::audit/profile-id (:id profile)})))
:else :else
(let [elapsed? (elapsed-verify-threshold? profile) (let [elapsed? (elapsed-verify-threshold? profile)
bounce? (eml/has-bounce-reports? conn (:email profile)) complaints? (eml/has-reports? conn (:email profile))
action (if bounce? action (if complaints?
"ignore-because-bounce" "ignore-because-complaints"
(if elapsed? (if elapsed?
"resend-email-verification" "resend-email-verification"
"ignore"))] "ignore"))]
(l/wrn :hint "repeated registry detected" (l/wrn :hint "repeated registry detected"
:profile-id (str (:id profile)) :profile-id (str (:id profile))
@ -446,7 +460,7 @@
;; ---- COMMAND: Request Profile Recovery ;; ---- COMMAND: Request Profile Recovery
(defn- request-profile-recovery (defn- request-profile-recovery
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}] [{:keys [::db/conn] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}] (letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens/generate (::setup/props cfg) (let [token (tokens/generate (::setup/props cfg)
{:iss :password-recovery {:iss :password-recovery
@ -468,39 +482,42 @@
:extra-data ptoken}) :extra-data ptoken})
nil))] nil))]
(db/with-atomic [conn pool] (let [profile (->> (profile/clean-email email)
(let [profile (->> (profile/clean-email email) (profile/get-profile-by-email conn))]
(profile/get-profile-by-email conn))]
(cond (cond
(not profile) (not profile)
(l/wrn :hint "attempt of profile recovery: no profile found" (l/wrn :hint "attempt of profile recovery: no profile found"
:profile-email email) :profile-email email)
(not (eml/allow-send-emails? conn profile)) (not (eml/allow-send-emails? conn profile))
(l/wrn :hint "attempt of profile recovery: profile is muted" (l/wrn :hint "attempt of profile recovery: profile is muted"
:profile-id (str (:id profile)) :profile-id (str (:id profile))
:profile-email (:email profile)) :profile-email (:email profile))
(eml/has-bounce-reports? conn (:email profile)) (eml/has-bounce-reports? conn (:email profile))
(l/wrn :hint "attempt of profile recovery: email has bounces" (l/wrn :hint "attempt of profile recovery: email has bounces"
:profile-id (str (:id profile)) :profile-id (str (:id profile))
:profile-email (:email profile)) :profile-email (:email profile))
(not (elapsed-verify-threshold? profile)) (eml/has-complaint-reports? conn (:email profile))
(l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed" (l/wrn :hint "attempt of profile recovery: email has complaints"
:profile-id (str (:id profile)) :profile-id (str (:id profile))
:profile-email (:email profile)) :profile-email (:email profile))
(not (elapsed-verify-threshold? profile))
(l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed"
:profile-id (str (:id profile))
:profile-email (:email profile))
:else :else
(do (do
(db/update! conn :profile (db/update! conn :profile
{:modified-at (dt/now)} {:modified-at (dt/now)}
{:id (:id profile)}) {:id (:id profile)})
(->> profile (->> profile
(create-recovery-token) (create-recovery-token)
(send-email-notification conn)))))))) (send-email-notification conn)))))))
(def schema:request-profile-recovery (def schema:request-profile-recovery
@ -512,6 +529,6 @@
::doc/added "1.15" ::doc/added "1.15"
::sm/params schema:request-profile-recovery} ::sm/params schema:request-profile-recovery}
[cfg params] [cfg params]
(request-profile-recovery cfg params)) (db/tx-run! cfg request-profile-recovery params))

View file

@ -276,19 +276,19 @@
(sv/defmethod ::request-email-change (sv/defmethod ::request-email-change
{::doc/added "1.0" {::doc/added "1.0"
::sm/params schema:request-email-change} ::sm/params schema:request-email-change}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id email] :as params}] [cfg {:keys [::rpc/profile-id email] :as params}]
(db/with-atomic [conn pool] (db/tx-run! cfg
(let [profile (db/get-by-id conn :profile profile-id) (fn [cfg]
cfg (assoc cfg ::conn conn) (let [profile (db/get-by-id cfg :profile profile-id)
params (assoc params params (assoc params
:profile profile :profile profile
:email (clean-email email))] :email (clean-email email))]
(if (contains? cf/flags :smtp) (if (contains? cf/flags :smtp)
(request-email-change! cfg params) (request-email-change! cfg params)
(change-email-immediately! cfg params))))) (change-email-immediately! cfg params))))))
(defn- change-email-immediately! (defn- change-email-immediately!
[{:keys [::conn]} {:keys [profile email] :as params}] [{:keys [::db/conn]} {:keys [profile email] :as params}]
(when (not= email (:email profile)) (when (not= email (:email profile))
(check-profile-existence! conn params)) (check-profile-existence! conn params))
@ -299,7 +299,7 @@
{:changed true}) {:changed true})
(defn- request-email-change! (defn- request-email-change!
[{:keys [::conn] :as cfg} {:keys [profile email] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile email] :as params}]
(let [token (tokens/generate (::setup/props cfg) (let [token (tokens/generate (::setup/props cfg)
{:iss :change-email {:iss :change-email
:exp (dt/in-future "15m") :exp (dt/in-future "15m")
@ -319,9 +319,28 @@
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces.")) :hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(when (eml/has-bounce-reports? conn email) (when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation (ex/raise :type :restriction
:code :email-has-permanent-bounces :code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) :email email
:hint "looks like the email has bounce reports"))
(when (eml/has-complaint-reports? conn email)
(ex/raise :type :restriction
:code :email-has-complaints
:email email
:hint "looks like the email has spam complaint reports"))
(when (eml/has-bounce-reports? conn (:email profile))
(ex/raise :type :restriction
:code :email-has-permanent-bounces
:email (:email profile)
:hint "looks like the email has bounce reports"))
(when (eml/has-complaint-reports? conn (:email profile))
(ex/raise :type :restriction
:code :email-has-complaints
:email (:email profile)
:hint "looks like the email has spam complaint reports"))
(eml/send! {::eml/conn conn (eml/send! {::eml/conn conn
::eml/factory eml/change-email ::eml/factory eml/change-email

View file

@ -734,12 +734,19 @@
:email email :email email
:hint "the profile has reported repeatedly as spam or has bounces")) :hint "the profile has reported repeatedly as spam or has bounces"))
;; Secondly check if the invited member email is part of the global spam/bounce report. ;; Secondly check if the invited member email is part of the global bounce report.
(when (eml/has-bounce-reports? conn email) (when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation (ex/raise :type :restriction
:code :email-has-permanent-bounces :code :email-has-permanent-bounces
:email email :email email
:hint "the email you invite has been repeatedly reported as spam or bounce")) :hint "the email you invite has been repeatedly reported as bounce"))
;; Secondly check if the invited member email is part of the global complain report.
(when (eml/has-complaint-reports? conn email)
(ex/raise :type :restriction
:code :email-has-complaints
:email email
:hint "the email you invite has been repeatedly reported as spam"))
;; When we have email verification disabled and invitation user is ;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the ;; already present in the database, we proceed to add it to the

View file

@ -590,9 +590,10 @@
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"}) (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
(let [out (th/command! data)] (let [out (th/command! data)]
(t/is (th/success? out)) (t/is (not (th/success? out)))
(let [result (:result out)] (let [edata (-> out :error ex-data)]
(t/is (contains? result :token)))))) (t/is (= :restriction (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata)))))))
(t/deftest register-profile-with-complained-email (t/deftest register-profile-with-complained-email
(let [pool (:app.db/pool th/*system*) (let [pool (:app.db/pool th/*system*)
@ -603,9 +604,11 @@
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"}) (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
(let [out (th/command! data)] (let [out (th/command! data)]
(t/is (th/success? out)) (t/is (not (th/success? out)))
(let [result (:result out)]
(t/is (contains? result :token)))))) (let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-complaints (:code edata)))))))
(t/deftest register-profile-with-email-as-password (t/deftest register-profile-with-email-as-password
(let [data {::th/type :prepare-register-profile (let [data {::th/type :prepare-register-profile
@ -636,20 +639,26 @@
;; with complaints ;; with complaints
(th/create-global-complaint-for pool {:type :complaint :email (:email data)}) (th/create-global-complaint-for pool {:type :complaint :email (:email data)})
(let [out (th/command! data)] (let [out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:result out))) (t/is (nil? (:result out)))
(t/is (= 2 (:call-count @mock))))
(let [edata (-> out :error ex-data)]
(t/is (= :restriction (:type edata)))
(t/is (= :email-has-complaints (:code edata))))
(t/is (= 1 (:call-count @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)})
(let [out (th/command! data) (let [out (th/command! data)]
error (:error out)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation)) (let [edata (-> out :error ex-data)]
(t/is (th/ex-of-code? error :email-has-permanent-bounces)) (t/is (= :restriction (:type edata)))
(t/is (= 2 (:call-count @mock))))))) (t/is (= :email-has-permanent-bounces (:code edata))))
(t/is (= 1 (:call-count @mock)))))))
(t/deftest email-change-request-without-smtp (t/deftest email-change-request-without-smtp
@ -714,7 +723,7 @@
out (th/command! data)] out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:result out))) (t/is (nil? (:result out)))
(t/is (= 2 (:call-count @mock)))) (t/is (= 1 (:call-count @mock))))
;; with valid email and active user with global bounce ;; with valid email and active user with global bounce
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)}) (th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
@ -723,7 +732,7 @@
(t/is (nil? (:result out))) (t/is (nil? (:result out)))
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (= 2 (:call-count @mock)))))))) (t/is (= 1 (:call-count @mock))))))))
(t/deftest update-profile-password (t/deftest update-profile-password

View file

@ -62,8 +62,8 @@
(th/reset-mock! mock) (th/reset-mock! mock)
(let [data (assoc data :emails ["foo@bar.com"]) (let [data (assoc data :emails ["foo@bar.com"])
out (th/command! data)] out (th/command! data)]
(t/is (th/success? out)) (t/is (not (th/success? out)))
(t/is (= 1 (:call-count (deref mock))))) (t/is (= 0 (:call-count (deref mock)))))
;; get invitation token ;; get invitation token
(let [params {::th/type :get-team-invitation-token (let [params {::th/type :get-team-invitation-token
@ -86,7 +86,7 @@
(t/is (= 0 (:call-count @mock))) (t/is (= 0 (:call-count @mock)))
(let [edata (-> out :error ex-data)] (let [edata (-> out :error ex-data)]
(t/is (= :validation (:type edata))) (t/is (= :restriction (:type edata)))
(t/is (= :email-has-permanent-bounces (:code edata))))) (t/is (= :email-has-permanent-bounces (:code edata)))))
;; invite internal user that is muted ;; invite internal user that is muted

View file

@ -59,7 +59,8 @@
:profile-is-muted :profile-is-muted
(rx/of (msg/error (tr "errors.profile-is-muted"))) (rx/of (msg/error (tr "errors.profile-is-muted")))
:email-has-permanent-bounces (:email-has-permanent-bounces
:email-has-complaints)
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data)))) (rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data))))
(rx/throw cause))))) (rx/throw cause)))))

View file

@ -54,7 +54,7 @@
(defn- on-prepare-register-error (defn- on-prepare-register-error
[form cause] [form cause]
(let [{:keys [type code]} (ex-data cause)] (let [{:keys [type code] :as edata} (ex-data cause)]
(condp = [type code] (condp = [type code]
[:restriction :registration-disabled] [:restriction :registration-disabled]
(st/emit! (msg/error (tr "errors.registration-disabled"))) (st/emit! (msg/error (tr "errors.registration-disabled")))
@ -62,6 +62,12 @@
[:restriction :email-domain-is-not-allowed] [:restriction :email-domain-is-not-allowed]
(st/emit! (msg/error (tr "errors.email-domain-not-allowed"))) (st/emit! (msg/error (tr "errors.email-domain-not-allowed")))
[:restriction :email-has-permanent-bounces]
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" (:email edata))))
[:restriction :email-has-complaints]
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" (:email edata))))
[:validation :email-as-password] [:validation :email-as-password]
(swap! form assoc-in [:errors :password] (swap! form assoc-in [:errors :password]
{:message "errors.email-as-password"}) {:message "errors.email-as-password"})

View file

@ -33,7 +33,6 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private arrow-icon (def ^:private arrow-icon
(i/icon-xref :arrow (stl/css :arrow-icon))) (i/icon-xref :arrow (stl/css :arrow-icon)))
@ -62,10 +61,10 @@
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]
::mf/wrap-props false} ::mf/wrap-props false}
[{:keys [section team]}] [{:keys [section team]}]
(let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members))) (let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings))) on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations))) on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks))) on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
members-section? (= section :dashboard-team-members) members-section? (= section :dashboard-team-members)
settings-section? (= section :dashboard-team-settings) settings-section? (= section :dashboard-team-settings)
@ -157,21 +156,22 @@
(dd/fetch-team-invitations))) (dd/fetch-team-invitations)))
on-error on-error
(fn [{:keys [type code] :as error}] (fn [_form cause]
(cond (let [{:keys [type code] :as error} (ex-data cause)]
(and (= :validation type) (cond
(= :profile-is-muted code)) (and (= :validation type)
(st/emit! (msg/error (tr "errors.profile-is-muted")) (= :profile-is-muted code))
(modal/hide)) (st/emit! (msg/error (tr "errors.profile-is-muted"))
(modal/hide))
(and (= :validation type) (or (= :member-is-muted code)
(or (= :member-is-muted code) (= :email-has-permanent-bounces code)
(= :email-has-permanent-bounces code))) (= :email-has-complaints code))
(swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error))) (swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error)))
:else :else
(st/emit! (msg/error (tr "errors.generic")) (st/emit! (msg/error (tr "errors.generic"))
(modal/hide)))) (modal/hide)))))
on-submit on-submit
(fn [form] (fn [form]
@ -563,22 +563,24 @@
on-error on-error
(mf/use-fn (mf/use-fn
(mf/deps email) (mf/deps email)
(fn [{:keys [type code] :as error}] (fn [cause]
(cond (let [{:keys [type code] :as error} (ex-data cause)]
(and (= :validation type) (cond
(= :profile-is-muted code)) (and (= :validation type)
(rx/of (msg/error (tr "errors.profile-is-muted"))) (= :profile-is-muted code))
(rx/of (msg/error (tr "errors.profile-is-muted")))
(and (= :validation type) (and (= :validation type)
(= :member-is-muted code)) (= :member-is-muted code))
(rx/of (msg/error (tr "errors.member-is-muted"))) (rx/of (msg/error (tr "errors.member-is-muted")))
(and (= :validation type) (and (= :restriction type)
(= :email-has-permanent-bounces code)) (or (= :email-has-permanent-bounces code)
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email))) (= :email-has-complaints code)))
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email)))
:else :else
(rx/throw error)))) (rx/throw cause)))))
on-delete on-delete
(mf/use-fn (mf/use-fn
@ -588,7 +590,6 @@
mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}] mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}]
(st/emit! (dd/delete-team-invitation (with-meta params mdata)))))) (st/emit! (dd/delete-team-invitation (with-meta params mdata))))))
on-resend-success on-resend-success
(mf/use-fn (mf/use-fn
(fn [] (fn []

View file

@ -70,8 +70,9 @@
(on-update-submit form) (on-update-submit form)
(on-create-submit form)))) (on-create-submit form))))
(mf/defc team-form-modal {::mf/register modal/components (mf/defc team-form-modal
::mf/register-as :team-form} {::mf/register modal/components
::mf/register-as :team-form}
[{:keys [team] :as props}] [{:keys [team] :as props}]
(let [initial (mf/use-memo (fn [] (or team {}))) (let [initial (mf/use-memo (fn [] (or team {})))
form (fm/use-form :spec ::team-form form (fm/use-form :spec ::team-form

View file

@ -39,21 +39,22 @@
(s/keys :req-un [::email-1 ::email-2])) (s/keys :req-un [::email-1 ::email-2]))
(defn- on-error (defn- on-error
[form error] [form cause]
(case (:code (ex-data error)) (let [{:keys [code] :as error} (ex-data cause)]
:email-already-exists (case code
(swap! form (fn [data] :email-already-exists
(let [error {:message (tr "errors.email-already-exists")}] (swap! form (fn [data]
(assoc-in data [:errors :email-1] error)))) (let [error {:message (tr "errors.email-already-exists")}]
(assoc-in data [:errors :email-1] error))))
:profile-is-muted :profile-is-muted
(rx/of (msg/error (tr "errors.profile-is-muted"))) (rx/of (msg/error (tr "errors.profile-is-muted")))
:email-has-permanent-bounces (:email-has-permanent-bounces
(let [email (get @form [:data :email-1])] :email-has-complaints)
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" email)))) (rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email error))))
(rx/throw error))) (rx/throw cause))))
(defn- on-success (defn- on-success
[profile data] [profile data]