0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-24 15:39:50 -05:00

Merge pull request #5137 from penpot/niwinz-enhancements-1

 Add limits for invitation creation RPC method
This commit is contained in:
Alejandro 2024-10-03 07:18:18 +02:00 committed by GitHub
commit ae7e28b71b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 68 additions and 30 deletions

View file

@ -6,6 +6,7 @@
- Fix problem with Ctrl+F shortcut on the dashboard [Taiga #8876](https://tree.taiga.io/project/penpot/issue/8876) - Fix problem with Ctrl+F shortcut on the dashboard [Taiga #8876](https://tree.taiga.io/project/penpot/issue/8876)
- Fix visual problem with the font-size dropdown in assets [Taiga #8872](https://tree.taiga.io/project/penpot/issue/8872) - Fix visual problem with the font-size dropdown in assets [Taiga #8872](https://tree.taiga.io/project/penpot/issue/8872)
- Add limits for invitation RPC methods (hard limit 25 emails per request)
## 2.2.0 ## 2.2.0

View file

@ -908,6 +908,10 @@
[:role schema:role] [:role schema:role]
[:emails [::sm/set ::sm/email]]]) [:emails [::sm/set ::sm/email]]])
(def ^:private max-invitations-by-request-threshold
"The number of invitations can be sent in a single rpc request"
25)
(sv/defmethod ::create-team-invitations (sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to "A rpc call that allow to send a single or multiple invitations to
join the team." join the team."
@ -920,6 +924,12 @@
team (db/get-by-id conn :team team-id) team (db/get-by-id conn :team team-id)
emails (into #{} (map profile/clean-email) emails)] emails (into #{} (map profile/clean-email) emails)]
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(run! (partial quotes/check-quote! conn) (run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/invitations-per-team (list {::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id ::quotes/profile-id profile-id
@ -994,6 +1004,12 @@
profile (db/get-by-id conn :profile profile-id) profile (db/get-by-id conn :profile profile-id)
emails (into #{} (map profile/clean-email) emails)] emails (into #{} (map profile/clean-email) emails)]
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(let [props {:name name :features features} (let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params) event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team") (assoc ::audit/name "create-team")

View file

@ -448,7 +448,7 @@
(defn parse-email (defn parse-email
[s] [s]
(if (string? s) (if (string? s)
(re-matches email-re s) (first (re-seq email-re s))
nil)) nil))
(defn email-string? (defn email-string?

View file

@ -10,7 +10,6 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.spec :as us]
[app.config :as cfg] [app.config :as cfg]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.events :as ev] [app.main.data.events :as ev]
@ -30,7 +29,6 @@
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -129,17 +127,10 @@
] ]
(filterv identity))) (filterv identity)))
(s/def ::emails (s/and ::us/set-of-valid-emails d/not-empty?))
(s/def ::role ::us/keyword)
(s/def ::team-id ::us/uuid)
(s/def ::invite-member-form
(s/keys :req-un [::role ::emails ::team-id]))
(def ^:private schema:invite-member-form (def ^:private schema:invite-member-form
[:map {:title "InviteMemberForm"} [:map {:title "InviteMemberForm"}
[:role :keyword] [:role :keyword]
[:emails [::sm/set {:kind ::sm/email :min 1}]] [:emails [::sm/set {:min 1} ::sm/email]]
[:team-id ::sm/uuid]]) [:team-id ::sm/uuid]])
(mf/defc invite-members-modal (mf/defc invite-members-modal
@ -181,6 +172,10 @@
(st/emit! (ntf/error (tr "errors.profile-is-muted")) (st/emit! (ntf/error (tr "errors.profile-is-muted"))
(modal/hide)) (modal/hide))
(and (= :validation type)
(= :max-invitations-by-request code))
(swap! error-text (tr "errors.maximum-invitations-by-request-reached" (:threshold error)))
(or (= :member-is-muted code) (or (= :member-is-muted code)
(= :email-has-permanent-bounces code) (= :email-has-permanent-bounces code)
(= :email-has-complaints code)) (= :email-has-complaints code))
@ -226,10 +221,9 @@
:name :emails :name :emails
:auto-focus? true :auto-focus? true
:trim true :trim true
:valid-item-fn us/parse-email :valid-item-fn sm/parse-email
:caution-item-fn current-members-emails :caution-item-fn current-members-emails
:label (tr "modals.invite-member.emails") :label (tr "modals.invite-member.emails")
:on-submit on-submit
:invite-email invite-email}]] :invite-email invite-email}]]
[:div {:class (stl/css :action-buttons)} [:div {:class (stl/css :action-buttons)}

View file

@ -11,11 +11,11 @@
[app.common.schema :as sm] [app.common.schema :as sm]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.notifications.context-notification :refer [context-notification]]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
@ -57,7 +57,7 @@
(def ^:private schema:invite-form (def ^:private schema:invite-form
[:map {:title "InviteForm"} [:map {:title "InviteForm"}
[:role :keyword] [:role :keyword]
[:emails {:optional true} [::sm/set {:kind ::sm/email}]]]) [:emails {:optional true} [::sm/set ::sm/email]]])
(defn- get-available-roles (defn- get-available-roles
[] []
@ -67,17 +67,14 @@
(mf/defc team-form-step-2 (mf/defc team-form-step-2
{::mf/props :obj} {::mf/props :obj}
[{:keys [name on-back go-to-team?]}] [{:keys [name on-back go-to-team?]}]
(let [initial (mf/use-memo (let [initial (mf/with-memo []
#(do {:role "editor" {:role "editor" :name name})
:name name}))
form (fm/use-form :schema schema:invite-form form (fm/use-form :schema schema:invite-form
:initial initial) :initial initial)
params (:clean-data @form)
emails (:emails params)
roles (mf/use-memo get-available-roles) roles (mf/use-memo get-available-roles)
error* (mf/use-state nil)
on-success on-success
(mf/use-fn (mf/use-fn
@ -90,8 +87,24 @@
on-error on-error
(mf/use-fn (mf/use-fn
(fn [_] (fn [cause]
(st/emit! (ntf/error (tr "errors.generic"))))) (let [{:keys [type code] :as error} (ex-data cause)]
(cond
(and (= :validation type)
(= :profile-is-muted code))
(swap! error* (tr "errors.profile-is-muted"))
(and (= :validation type)
(= :max-invitations-by-request code))
(swap! error* (tr "errors.maximum-invitations-by-request-reached" (:threshold error)))
(or (= :member-is-muted code)
(= :email-has-permanent-bounces code)
(= :email-has-complaints code))
(swap! error* (tr "errors.email-spam-or-permanent-bounces" (:email error)))
:else
(swap! error* (tr "errors.generic"))))))
on-invite-later on-invite-later
(mf/use-fn (mf/use-fn
@ -111,7 +124,7 @@
on-invite-now on-invite-now
(mf/use-fn (mf/use-fn
(fn [{:keys [name] :as params}] (fn [{:keys [name emails] :as params}]
(let [mdata {:on-success on-success (let [mdata {:on-success on-success
:on-error on-error}] :on-error on-error}]
@ -143,6 +156,10 @@
[:& fm/form {:form form [:& fm/form {:form form
:class (stl/css :modal-form-invitations) :class (stl/css :modal-form-invitations)
:on-submit on-submit} :on-submit on-submit}
(when-let [content (deref error*)]
[:& context-notification {:content content :level :error}])
[:div {:class (stl/css :role-select)} [:div {:class (stl/css :role-select)}
[:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")] [:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")]
[:& fm/select {:name :role :options roles}]] [:& fm/select {:name :role :options roles}]]
@ -155,18 +172,22 @@
:valid-item-fn sm/parse-email :valid-item-fn sm/parse-email
:caution-item-fn #{} :caution-item-fn #{}
:label (tr "modals.invite-member.emails") :label (tr "modals.invite-member.emails")
:on-submit on-submit}]] ;; :on-submit on-submit
}]]
[:div {:class (stl/css :action-buttons)} [:div {:class (stl/css :action-buttons)}
[:button {:class (stl/css :back-button) [:button {:class (stl/css :back-button)
:on-click on-back} :on-click on-back}
(tr "labels.back")] (tr "labels.back")]
[:> fm/submit-button* (let [params (:clean-data @form)
{:class (stl/css :accept-button) emails (:emails params)]
:label (if (> (count emails) 0) [:> fm/submit-button*
(tr "onboarding.choice.team-up.create-team-and-invite") {:class (stl/css :accept-button)
(tr "onboarding.choice.team-up.create-team-without-invite"))}]] :label (if (> (count emails) 0)
(tr "onboarding.choice.team-up.create-team-and-invite")
(tr "onboarding.choice.team-up.create-team-without-invite"))}])]
[:div {:class (stl/css :modal-hint)} [:div {:class (stl/css :modal-hint)}
"(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"]]] "(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"]]]

View file

@ -923,6 +923,9 @@ msgstr "Are you sure?"
msgid "errors.auth-provider-not-allowed" msgid "errors.auth-provider-not-allowed"
msgstr "Auth provider not allowed for this profile" msgstr "Auth provider not allowed for this profile"
msgid "errors.maximum-invitations-by-request-reached"
msgstr "The maximum (%s) number of emails that can be invited in a single request has been reached"
#: src/app/main/ui/auth/login.cljs:61 #: src/app/main/ui/auth/login.cljs:61
msgid "errors.auth-provider-not-configured" msgid "errors.auth-provider-not-configured"
msgstr "Authentication provider not configured." msgstr "Authentication provider not configured."

View file

@ -6094,3 +6094,6 @@ msgstr "Actualizar"
#, unused #, unused
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta" msgstr "Pulsar para cerrar la ruta"
msgid "errors.maximum-invitations-by-request-reached"
msgstr "Se ha alcanzado el número máximo (%s) de correos electrónicos que se pueden invitar en una sola solicitud"