Skip to content

Commit

Permalink
✨ Add limits for team invitations
Browse files Browse the repository at this point in the history
  • Loading branch information
niwinz committed Oct 2, 2024
1 parent 8373654 commit be30174
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- 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)
- Add limits for invitation RPC methods (hard limit 25 emails per request)

## 2.2.0

Expand Down
16 changes: 16 additions & 0 deletions backend/src/app/rpc/commands/teams.clj
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,10 @@
[:role schema:role]
[: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
"A rpc call that allow to send a single or multiple invitations to
join the team."
Expand All @@ -920,6 +924,12 @@
team (db/get-by-id conn :team team-id)
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)
(list {::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
Expand Down Expand Up @@ -994,6 +1004,12 @@
profile (db/get-by-id conn :profile profile-id)
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}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team")
Expand Down
2 changes: 1 addition & 1 deletion common/src/app/common/schema.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@
(defn parse-email
[s]
(if (string? s)
(re-matches email-re s)
(first (re-seq email-re s))
nil))

(defn email-string?
Expand Down
18 changes: 6 additions & 12 deletions frontend/src/app/main/ui/dashboard/team.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.config :as cfg]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
Expand All @@ -30,7 +29,6 @@
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[beicon.v2.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.v2 :as mf]))

Expand Down Expand Up @@ -129,17 +127,10 @@
]
(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
[:map {:title "InviteMemberForm"}
[:role :keyword]
[:emails [::sm/set {:kind ::sm/email :min 1}]]
[:emails [::sm/set {:min 1} ::sm/email]]
[:team-id ::sm/uuid]])

(mf/defc invite-members-modal
Expand Down Expand Up @@ -181,6 +172,10 @@
(st/emit! (ntf/error (tr "errors.profile-is-muted"))
(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)
(= :email-has-permanent-bounces code)
(= :email-has-complaints code))
Expand Down Expand Up @@ -226,10 +221,9 @@
:name :emails
:auto-focus? true
:trim true
:valid-item-fn us/parse-email
:valid-item-fn sm/parse-email
:caution-item-fn current-members-emails
:label (tr "modals.invite-member.emails")
:on-submit on-submit
:invite-email invite-email}]]

[:div {:class (stl/css :action-buttons)}
Expand Down
55 changes: 38 additions & 17 deletions frontend/src/app/main/ui/onboarding/team_choice.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
[app.common.schema :as sm]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i]
[app.main.ui.notifications.context-notification :refer [context-notification]]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
[potok.v2.core :as ptk]
Expand Down Expand Up @@ -57,7 +57,7 @@
(def ^:private schema:invite-form
[:map {:title "InviteForm"}
[:role :keyword]
[:emails {:optional true} [::sm/set {:kind ::sm/email}]]])
[:emails {:optional true} [::sm/set ::sm/email]]])

(defn- get-available-roles
[]
Expand All @@ -67,17 +67,14 @@
(mf/defc team-form-step-2
{::mf/props :obj}
[{:keys [name on-back go-to-team?]}]
(let [initial (mf/use-memo
#(do {:role "editor"
:name name}))
(let [initial (mf/with-memo []
{:role "editor" :name name})

form (fm/use-form :schema schema:invite-form
:initial initial)

params (:clean-data @form)
emails (:emails params)

roles (mf/use-memo get-available-roles)
error* (mf/use-state nil)

on-success
(mf/use-fn
Expand All @@ -90,8 +87,24 @@

on-error
(mf/use-fn
(fn [_]
(st/emit! (ntf/error (tr "errors.generic")))))
(fn [cause]
(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
(mf/use-fn
Expand All @@ -111,7 +124,7 @@

on-invite-now
(mf/use-fn
(fn [{:keys [name] :as params}]
(fn [{:keys [name emails] :as params}]
(let [mdata {:on-success on-success
:on-error on-error}]

Expand Down Expand Up @@ -143,6 +156,10 @@
[:& fm/form {:form form
:class (stl/css :modal-form-invitations)
:on-submit on-submit}

(when-let [content (deref error*)]
[:& context-notification {:content content :level :error}])

[:div {:class (stl/css :role-select)}
[:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")]
[:& fm/select {:name :role :options roles}]]
Expand All @@ -155,18 +172,22 @@
:valid-item-fn sm/parse-email
:caution-item-fn #{}
:label (tr "modals.invite-member.emails")
:on-submit on-submit}]]
;; :on-submit on-submit
}]]

[:div {:class (stl/css :action-buttons)}
[:button {:class (stl/css :back-button)
:on-click on-back}
(tr "labels.back")]

[:> fm/submit-button*
{:class (stl/css :accept-button)
:label (if (> (count emails) 0)
(tr "onboarding.choice.team-up.create-team-and-invite")
(tr "onboarding.choice.team-up.create-team-without-invite"))}]]
(let [params (:clean-data @form)
emails (:emails params)]
[:> fm/submit-button*
{:class (stl/css :accept-button)
: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)}
"(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"]]]

Expand Down
3 changes: 3 additions & 0 deletions frontend/translations/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,9 @@ msgstr "Are you sure?"
msgid "errors.auth-provider-not-allowed"
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
msgid "errors.auth-provider-not-configured"
msgstr "Authentication provider not configured."
Expand Down
3 changes: 3 additions & 0 deletions frontend/translations/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -6094,3 +6094,6 @@ msgstr "Actualizar"
#, unused
msgid "workspace.viewport.click-to-close-path"
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"

0 comments on commit be30174

Please sign in to comment.