mirror of
https://github.com/penpot/penpot.git
synced 2026-09-09 21:49:10 +00:00
🐛 Add cooldown to prevent duplicate invitation emails (#11063)
This commit is contained in:
parent
5906312dff
commit
bf62e59f73
@ -51,6 +51,25 @@
|
|||||||
update set role = ?, valid_until = ?, updated_at = now()
|
update set role = ?, valid_until = ?, updated_at = now()
|
||||||
returning *")
|
returning *")
|
||||||
|
|
||||||
|
(def ^:private sql:check-recent-invitation
|
||||||
|
"SELECT 1 FROM team_invitation
|
||||||
|
WHERE team_id = ? AND email_to = ?
|
||||||
|
AND updated_at > now() - interval '5 minutes'
|
||||||
|
LIMIT 1")
|
||||||
|
|
||||||
|
(def ^:private sql:check-recent-org-invitation
|
||||||
|
"SELECT 1 FROM team_invitation
|
||||||
|
WHERE org_id = ? AND email_to = ?
|
||||||
|
AND updated_at > now() - interval '5 minutes'
|
||||||
|
LIMIT 1")
|
||||||
|
|
||||||
|
(defn- recently-invited?
|
||||||
|
[{:keys [::db/conn]} team-id org-id email]
|
||||||
|
(let [query (if org-id
|
||||||
|
[sql:check-recent-org-invitation org-id email]
|
||||||
|
[sql:check-recent-invitation team-id email])]
|
||||||
|
(some? (db/exec-one! conn query))))
|
||||||
|
|
||||||
(defn- create-invitation-token
|
(defn- create-invitation-token
|
||||||
[cfg {:keys [profile-id valid-until organization-id organization-name team-id member-id member-email role]}]
|
[cfg {:keys [profile-id valid-until organization-id organization-name team-id member-id member-email role]}]
|
||||||
(tokens/generate cfg
|
(tokens/generate cfg
|
||||||
@ -189,6 +208,7 @@
|
|||||||
expire (if organization
|
expire (if organization
|
||||||
(ct/in-future "876000h") ;; Organization invitations doesn't expire
|
(ct/in-future "876000h") ;; Organization invitations doesn't expire
|
||||||
(ct/in-future "168h")) ;; 7 days
|
(ct/in-future "168h")) ;; 7 days
|
||||||
|
recent? (recently-invited? cfg (:id team) (:id organization) email)
|
||||||
invitation (db/exec-one! conn (if organization
|
invitation (db/exec-one! conn (if organization
|
||||||
[sql:upsert-organization-invitation id
|
[sql:upsert-organization-invitation id
|
||||||
(:id organization)
|
(:id organization)
|
||||||
@ -251,7 +271,8 @@
|
|||||||
(assoc :props props))]
|
(assoc :props props))]
|
||||||
(audit/submit cfg event))
|
(audit/submit cfg event))
|
||||||
|
|
||||||
(when (allow-invitation-emails? member)
|
(when (and (allow-invitation-emails? member)
|
||||||
|
(not recent?))
|
||||||
(if organization
|
(if organization
|
||||||
(when (contains? cf/flags :admin-console)
|
(when (contains? cf/flags :admin-console)
|
||||||
(eml/send! {::eml/conn conn
|
(eml/send! {::eml/conn conn
|
||||||
|
|||||||
@ -1015,6 +1015,46 @@
|
|||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
(t/is (th/success? out)))))
|
(t/is (th/success? out)))))
|
||||||
|
|
||||||
|
(t/deftest create-team-invitations-email-cooldown
|
||||||
|
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||||
|
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||||
|
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||||
|
|
||||||
|
data {::th/type :create-team-invitations
|
||||||
|
::rpc/profile-id (:id profile1)
|
||||||
|
:team-id (:id team)
|
||||||
|
:role :editor
|
||||||
|
:emails ["cooldown-test@example.com"]}]
|
||||||
|
|
||||||
|
;; First invitation sends email
|
||||||
|
(let [out (th/command! data)]
|
||||||
|
(t/is (th/success? out))
|
||||||
|
(t/is (= 1 (:call-count @mock))))
|
||||||
|
|
||||||
|
;; Resending immediately should NOT send email (cooldown active)
|
||||||
|
(th/reset-mock! mock)
|
||||||
|
(let [out (th/command! data)]
|
||||||
|
(t/is (th/success? out))
|
||||||
|
(t/is (= 0 (:call-count @mock))))
|
||||||
|
|
||||||
|
;; Resending to a different email should send email
|
||||||
|
(th/reset-mock! mock)
|
||||||
|
(let [data (assoc data :emails ["different@example.com"])
|
||||||
|
out (th/command! data)]
|
||||||
|
(t/is (th/success? out))
|
||||||
|
(t/is (= 1 (:call-count @mock))))
|
||||||
|
|
||||||
|
;; After cooldown expires, resending should send email
|
||||||
|
(th/reset-mock! mock)
|
||||||
|
(th/db-update! :team-invitation
|
||||||
|
{:updated-at (ct/in-past "10m")}
|
||||||
|
{:team-id (:id team)
|
||||||
|
:email-to "cooldown-test@example.com"})
|
||||||
|
(let [data (assoc data :emails ["cooldown-test@example.com"])
|
||||||
|
out (th/command! data)]
|
||||||
|
(t/is (th/success? out))
|
||||||
|
(t/is (= 1 (:call-count @mock)))))))
|
||||||
|
|
||||||
(t/deftest update-team-with-invalid-name
|
(t/deftest update-team-with-invalid-name
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
(let [profile (th/create-profile* 1 {:is-active true})
|
||||||
team (th/create-team* 1 {:profile-id (:id profile)})]
|
team (th/create-team* 1 {:profile-id (:id profile)})]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user