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:
commit
ae7e28b71b
7 changed files with 68 additions and 30 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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") ")"]]]
|
||||||
|
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Reference in a new issue