mirror of
https://github.com/penpot/penpot.git
synced 2026-09-25 21:36:15 +00:00
♻️ Build organization invitation audit event in frontend (#11825)
* ♻️ Build organization invitation audit event in frontend * ♻️ Align invitation token profile-id with created-by The invitation token carried the minter in :profile-id while the invitation row tracks the creator in :created-by. Both mean the inviter, so re-sends or re-requested links made them disagree and forced a second response key, :user-who-send-invitation. Mint :profile-id from :created-by in both token creators, backfill it from the row on accept (covers stale in-flight tokens), and drop the duplicate response key. The frontend maps :profile-id to the unchanged :user-who-send-invitation audit prop. AI-assisted-by: Muse Spark 1.3 Free * ♻️ Reuse token ids in invitation accept response Backfill :member-id with the accepting profile and drop the :user-id duplicate from the verify-token response, mirroring the :profile-id/:user-who-send-invitation cleanup. The frontend maps :member-id to the unchanged :user-id audit prop. AI-assisted-by: Muse Spark 1.3 Free --------- Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
62153dcb0b
commit
dfc1848ffc
@ -75,6 +75,9 @@
|
|||||||
(tokens/generate cfg
|
(tokens/generate cfg
|
||||||
{:iss :team-invitation
|
{:iss :team-invitation
|
||||||
:exp valid-until
|
:exp valid-until
|
||||||
|
;; NOTE: :profile-id is the inviter (the `created-by` of
|
||||||
|
;; the invitation row). Callers must pass the row value
|
||||||
|
;; so the token stays consistent with the database.
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:role role
|
:role role
|
||||||
:team-id team-id
|
:team-id team-id
|
||||||
@ -218,7 +221,7 @@
|
|||||||
updated? (not= id (:id invitation))
|
updated? (not= id (:id invitation))
|
||||||
profile-id (:id profile)
|
profile-id (:id profile)
|
||||||
team-organization-id (get-in team [:organization :id])
|
team-organization-id (get-in team [:organization :id])
|
||||||
tprops {:profile-id profile-id
|
tprops {:profile-id (or (:created-by invitation) profile-id)
|
||||||
:invitation-id (:id invitation)
|
:invitation-id (:id invitation)
|
||||||
:valid-until expire
|
:valid-until expire
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
@ -611,7 +614,11 @@
|
|||||||
|
|
||||||
member (profile/get-profile-by-email pool (:email-to invit))
|
member (profile/get-profile-by-email pool (:email-to invit))
|
||||||
token (create-invitation-token cfg {:team-id (:team-id invit)
|
token (create-invitation-token cfg {:team-id (:team-id invit)
|
||||||
:profile-id profile-id
|
;; The inviter is the creator of
|
||||||
|
;; the invitation row, not the
|
||||||
|
;; profile requesting the token.
|
||||||
|
:profile-id (or (:created-by invit)
|
||||||
|
profile-id)
|
||||||
:valid-until (:valid-until invit)
|
:valid-until (:valid-until invit)
|
||||||
:role (:role invit)
|
:role (:role invit)
|
||||||
:member-id (:id member)
|
:member-id (:id member)
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(ns app.rpc.commands.verify-token
|
(ns app.rpc.commands.verify-token
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
@ -174,6 +175,7 @@
|
|||||||
[:map {:title "TeamInvitationClaims"}
|
[:map {:title "TeamInvitationClaims"}
|
||||||
[:iss :keyword]
|
[:iss :keyword]
|
||||||
[:exp ::ct/inst]
|
[:exp ::ct/inst]
|
||||||
|
;; The inviter: always the `created-by` of the invitation row.
|
||||||
[:profile-id ::sm/uuid]
|
[:profile-id ::sm/uuid]
|
||||||
[:role types.team/schema:role]
|
[:role types.team/schema:role]
|
||||||
[:team-id {:optional true} ::sm/uuid]
|
[:team-id {:optional true} ::sm/uuid]
|
||||||
@ -244,18 +246,6 @@
|
|||||||
(not (:is-member membership)))
|
(not (:is-member membership)))
|
||||||
(:organization-id membership))
|
(:organization-id membership))
|
||||||
|
|
||||||
organization-add-source
|
|
||||||
(when organization-id-on-add
|
|
||||||
(if organization-id
|
|
||||||
"direct-organization-invitation"
|
|
||||||
"team-invitation"))
|
|
||||||
|
|
||||||
organization-event-origin
|
|
||||||
(when organization-id-on-add
|
|
||||||
(if organization-id
|
|
||||||
"organization-invitation-acceptance"
|
|
||||||
"team-invitation-acceptance"))
|
|
||||||
|
|
||||||
organization-member-count-before
|
organization-member-count-before
|
||||||
(when organization-id-on-add
|
(when organization-id-on-add
|
||||||
(count
|
(count
|
||||||
@ -313,25 +303,34 @@
|
|||||||
:user-who-send-invitation (:created-by invitation))
|
:user-who-send-invitation (:created-by invitation))
|
||||||
(audit/clean-props))))))
|
(audit/clean-props))))))
|
||||||
|
|
||||||
(cond-> (assoc claims :state :created)
|
(cond-> (assoc claims
|
||||||
|
:state :created
|
||||||
|
;; The invitation row is authoritative for the
|
||||||
|
;; inviter: backfill :profile-id so the response
|
||||||
|
;; stays consistent even with tokens minted
|
||||||
|
;; before :profile-id was aligned with
|
||||||
|
;; :created-by (or re-requested by someone else).
|
||||||
|
:profile-id (or (:created-by invitation)
|
||||||
|
(:profile-id claims))
|
||||||
|
;; Likewise, the accepting profile is
|
||||||
|
;; authoritative for the invitee: backfill
|
||||||
|
;; :member-id (nil for invitations sent to an
|
||||||
|
;; unregistered email, possibly stale
|
||||||
|
;; otherwise).
|
||||||
|
:member-id (:id profile))
|
||||||
;; when the invitation is to an organization, instead of a team, add the
|
;; when the invitation is to an organization, instead of a team, add the
|
||||||
;; accepted-team-id as :organization-team-id
|
;; accepted-team-id as :organization-team-id
|
||||||
(:organization-id claims)
|
(:organization-id claims)
|
||||||
(assoc :organization-team-id accepted-team-id)
|
(assoc :organization-team-id accepted-team-id)
|
||||||
|
|
||||||
organization-id-on-add
|
organization-id-on-add
|
||||||
(assoc :organization-invitation-audit
|
(merge (d/without-nils
|
||||||
{:origin organization-event-origin
|
{:invitation-id (:id invitation)
|
||||||
:props
|
:organization-member-count-before
|
||||||
(-> props
|
organization-member-count-before}))
|
||||||
(assoc :organization-id organization-id-on-add
|
|
||||||
:organization-member-add-source organization-add-source
|
(and organization-id-on-add team-id)
|
||||||
:belongs-to-team-on-add (boolean team-id)
|
(assoc :organization-id organization-id-on-add))))))
|
||||||
:user-id (:id profile)
|
|
||||||
:user-who-send-invitation (:created-by invitation)
|
|
||||||
:organization-member-count-before
|
|
||||||
organization-member-count-before)
|
|
||||||
(audit/clean-props))}))))))
|
|
||||||
|
|
||||||
(do
|
(do
|
||||||
;; If the user is not logged-in and the invitation has been canceled
|
;; If the user is not logged-in and the invitation has been canceled
|
||||||
|
|||||||
@ -498,7 +498,7 @@
|
|||||||
(map second)
|
(map second)
|
||||||
(filter #(= "accept-organization-invitation" (:name %)))
|
(filter #(= "accept-organization-invitation" (:name %)))
|
||||||
first))
|
first))
|
||||||
frontend-event (atom nil)]
|
token-result (atom nil)]
|
||||||
|
|
||||||
(db/insert! (:app.db/pool th/*system*)
|
(db/insert! (:app.db/pool th/*system*)
|
||||||
:team-invitation
|
:team-invitation
|
||||||
@ -520,8 +520,7 @@
|
|||||||
(fn [& _] default-team-id)]
|
(fn [& _] default-team-id)]
|
||||||
(let [out (verify! direct-token)]
|
(let [out (verify! direct-token)]
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(reset! frontend-event
|
(reset! token-result (:result out))))
|
||||||
(get-in out [:result :organization-invitation-audit]))))
|
|
||||||
|
|
||||||
(let [event (organization-event)]
|
(let [event (organization-event)]
|
||||||
(t/is (= organization-id (get-in event [:props :organization-id])))
|
(t/is (= organization-id (get-in event [:props :organization-id])))
|
||||||
@ -533,19 +532,17 @@
|
|||||||
(t/is (not (contains? (:props event) :organization-member-count-before)))
|
(t/is (not (contains? (:props event) :organization-member-count-before)))
|
||||||
(t/is (= :editor (get-in event [:props :role])))
|
(t/is (= :editor (get-in event [:props :role])))
|
||||||
(t/is (uuid? (get-in event [:props :invitation-id])))
|
(t/is (uuid? (get-in event [:props :invitation-id])))
|
||||||
(t/is (= "organization-invitation-acceptance"
|
(t/is (= organization-id (:organization-id @token-result)))
|
||||||
(:origin @frontend-event)))
|
(t/is (= :editor (:role @token-result)))
|
||||||
(t/is (= organization-id
|
(t/is (not (contains? @token-result :organization-invitation-audit)))
|
||||||
(get-in @frontend-event [:props :organization-id])))
|
(t/is (= (get-in event [:props :invitation-id])
|
||||||
|
(:invitation-id @token-result)))
|
||||||
(t/is (= (:id invitee)
|
(t/is (= (:id invitee)
|
||||||
(get-in @frontend-event [:props :user-id])))
|
(:member-id @token-result)))
|
||||||
(t/is (= (:id inviter)
|
(t/is (= (:id inviter)
|
||||||
(get-in @frontend-event [:props :user-who-send-invitation])))
|
(:profile-id @token-result)))
|
||||||
(t/is (= "direct-organization-invitation"
|
|
||||||
(get-in @frontend-event [:props :organization-member-add-source])))
|
|
||||||
(t/is (false? (get-in @frontend-event [:props :belongs-to-team-on-add])))
|
|
||||||
(t/is (= 3
|
(t/is (= 3
|
||||||
(get-in @frontend-event [:props :organization-member-count-before])))
|
(:organization-member-count-before @token-result)))
|
||||||
(t/is (not-any? #(contains? #{"accept-team-invitation"
|
(t/is (not-any? #(contains? #{"accept-team-invitation"
|
||||||
"accept-team-invitation-from"}
|
"accept-team-invitation-from"}
|
||||||
(:name (second %)))
|
(:name (second %)))
|
||||||
@ -572,8 +569,7 @@
|
|||||||
teams/add-profile-to-team! (fn [& _] nil)]
|
teams/add-profile-to-team! (fn [& _] nil)]
|
||||||
(let [out (verify! team-token)]
|
(let [out (verify! team-token)]
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(reset! frontend-event
|
(reset! token-result (:result out))))
|
||||||
(get-in out [:result :organization-invitation-audit]))))
|
|
||||||
|
|
||||||
(let [events (mapv second (:call-args-list @audit-mock))
|
(let [events (mapv second (:call-args-list @audit-mock))
|
||||||
event (organization-event)]
|
event (organization-event)]
|
||||||
@ -587,20 +583,19 @@
|
|||||||
(t/is (not (contains? (:props event) :organization-member-add-source)))
|
(t/is (not (contains? (:props event) :organization-member-add-source)))
|
||||||
(t/is (not (contains? (:props event) :belongs-to-team-on-add)))
|
(t/is (not (contains? (:props event) :belongs-to-team-on-add)))
|
||||||
(t/is (not (contains? (:props event) :organization-member-count-before)))
|
(t/is (not (contains? (:props event) :organization-member-count-before)))
|
||||||
(t/is (= "team-invitation-acceptance"
|
|
||||||
(:origin @frontend-event)))
|
|
||||||
(t/is (= (:id team) (get-in @frontend-event [:props :team-id])))
|
|
||||||
(t/is (= organization-id
|
(t/is (= organization-id
|
||||||
(get-in @frontend-event [:props :organization-id])))
|
(:organization-id @token-result)))
|
||||||
|
(t/is (= (:id team) (:team-id @token-result)))
|
||||||
|
(t/is (= :editor (:role @token-result)))
|
||||||
|
(t/is (not (contains? @token-result :organization-invitation-audit)))
|
||||||
|
(t/is (= (get-in event [:props :invitation-id])
|
||||||
|
(:invitation-id @token-result)))
|
||||||
(t/is (= (:id invitee)
|
(t/is (= (:id invitee)
|
||||||
(get-in @frontend-event [:props :user-id])))
|
(:member-id @token-result)))
|
||||||
(t/is (= (:id inviter)
|
(t/is (= (:id inviter)
|
||||||
(get-in @frontend-event [:props :user-who-send-invitation])))
|
(:profile-id @token-result)))
|
||||||
(t/is (= "team-invitation"
|
|
||||||
(get-in @frontend-event [:props :organization-member-add-source])))
|
|
||||||
(t/is (true? (get-in @frontend-event [:props :belongs-to-team-on-add])))
|
|
||||||
(t/is (= 5
|
(t/is (= 5
|
||||||
(get-in @frontend-event [:props :organization-member-count-before]))))
|
(:organization-member-count-before @token-result))))
|
||||||
|
|
||||||
(th/reset-mock! audit-mock)
|
(th/reset-mock! audit-mock)
|
||||||
(db/insert! (:app.db/pool th/*system*)
|
(db/insert! (:app.db/pool th/*system*)
|
||||||
@ -621,13 +616,57 @@
|
|||||||
teams/add-profile-to-team! (fn [& _] nil)]
|
teams/add-profile-to-team! (fn [& _] nil)]
|
||||||
(let [out (verify! team-token)]
|
(let [out (verify! team-token)]
|
||||||
(t/is (th/success? out))
|
(t/is (th/success? out))
|
||||||
(reset! frontend-event
|
(reset! token-result (:result out))))
|
||||||
(get-in out [:result :organization-invitation-audit]))))
|
|
||||||
|
|
||||||
(let [events (mapv second (:call-args-list @audit-mock))]
|
(let [events (mapv second (:call-args-list @audit-mock))]
|
||||||
(t/is (some #(= "accept-team-invitation" (:name %)) events))
|
(t/is (some #(= "accept-team-invitation" (:name %)) events))
|
||||||
(t/is (not-any? #(= "accept-organization-invitation" (:name %)) events))
|
(t/is (not-any? #(= "accept-organization-invitation" (:name %)) events))
|
||||||
(t/is (nil? @frontend-event))))))
|
(t/is (not (contains? @token-result :organization-invitation-audit)))
|
||||||
|
(t/is (not (contains? @token-result :organization-member-count-before)))))))
|
||||||
|
|
||||||
|
(t/deftest accept-organization-invitation-response-ids-match-database
|
||||||
|
(with-mocks [audit-mock {:target 'app.loggers.audit/submit :return nil}]
|
||||||
|
(let [inviter (th/create-profile* 211 {:is-active true})
|
||||||
|
invitee (th/create-profile* 212 {:is-active true})
|
||||||
|
organization-id (uuid/random)
|
||||||
|
default-team-id (uuid/random)
|
||||||
|
;; Token minted with a stale :profile-id (e.g. a re-sent link
|
||||||
|
;; requested by someone else) and no :member-id (invitee was
|
||||||
|
;; unregistered when invited); the invitation row says inviter.
|
||||||
|
stale-token (tokens/generate
|
||||||
|
th/*system*
|
||||||
|
{:iss :team-invitation
|
||||||
|
:exp (ct/in-future "1h")
|
||||||
|
:profile-id (uuid/random)
|
||||||
|
:role :editor
|
||||||
|
:organization-id organization-id
|
||||||
|
:member-email (:email invitee)})]
|
||||||
|
(db/insert! (:app.db/pool th/*system*)
|
||||||
|
:team-invitation
|
||||||
|
{:org-id organization-id
|
||||||
|
:email-to (:email invitee)
|
||||||
|
:created-by (:id inviter)
|
||||||
|
:role "editor"
|
||||||
|
:valid-until (ct/in-future "48h")})
|
||||||
|
|
||||||
|
(with-redefs [cf/flags (conj cf/flags :admin-console)
|
||||||
|
nitrate/call
|
||||||
|
(fn [_cfg method _params]
|
||||||
|
(case method
|
||||||
|
:get-organization-membership {:organization-id organization-id
|
||||||
|
:is-member false}
|
||||||
|
:get-organization-members [(:id inviter)]
|
||||||
|
nil))
|
||||||
|
teams/initialize-user-in-organization
|
||||||
|
(fn [& _] default-team-id)]
|
||||||
|
(let [out (th/command! {::th/type :verify-token
|
||||||
|
::rpc/profile-id (:id invitee)
|
||||||
|
:token stale-token})]
|
||||||
|
(t/is (th/success? out))
|
||||||
|
(t/is (= (:id inviter) (:profile-id (:result out))))
|
||||||
|
(t/is (= (:id invitee) (:member-id (:result out))))
|
||||||
|
(t/is (not (contains? (:result out) :user-who-send-invitation)))
|
||||||
|
(t/is (not (contains? (:result out) :user-id))))))))
|
||||||
|
|
||||||
(t/deftest create-team-invitations-with-email-verification-disabled
|
(t/deftest create-team-invitations-with-email-verification-disabled
|
||||||
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
(with-mocks [mock {:target 'app.email/send! :return nil}]
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(ns app.main.ui.auth.verify-token
|
(ns app.main.ui.auth.verify-token
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.main.data.auth :as da]
|
[app.main.data.auth :as da]
|
||||||
[app.main.data.common :as dcm]
|
[app.main.data.common :as dcm]
|
||||||
@ -45,12 +46,31 @@
|
|||||||
|
|
||||||
(defmethod handle-token :team-invitation
|
(defmethod handle-token :team-invitation
|
||||||
[{:keys [state team-id organization-team-id organization-name invitation-token] :as tdata}]
|
[{:keys [state team-id organization-team-id organization-name invitation-token] :as tdata}]
|
||||||
(when-let [{:keys [origin props]} (:organization-invitation-audit tdata)]
|
(when (and (= state :created)
|
||||||
(st/emit!
|
(contains? tdata :organization-member-count-before))
|
||||||
(ev/event
|
(let [direct-invitation? (some? organization-team-id)]
|
||||||
(assoc props
|
(st/emit!
|
||||||
::ev/name "accept-organization-invitation"
|
(ev/event
|
||||||
::ev/origin origin))))
|
(-> (select-keys tdata [:team-id :organization-id :role
|
||||||
|
:invitation-id :member-id :profile-id
|
||||||
|
:organization-member-count-before])
|
||||||
|
;; The audit event keeps its own names: :user-id is the
|
||||||
|
;; invitee (:member-id, backfilled by the backend with the
|
||||||
|
;; accepting profile) and :user-who-send-invitation is the
|
||||||
|
;; inviter (:profile-id, backfilled from `created-by`).
|
||||||
|
(assoc :user-id (:member-id tdata)
|
||||||
|
:user-who-send-invitation (:profile-id tdata))
|
||||||
|
(dissoc :member-id :profile-id)
|
||||||
|
(d/without-nils)
|
||||||
|
(assoc :organization-member-add-source
|
||||||
|
(if direct-invitation?
|
||||||
|
"direct-organization-invitation"
|
||||||
|
"team-invitation")
|
||||||
|
:belongs-to-team-on-add (boolean team-id)
|
||||||
|
::ev/name "accept-organization-invitation"
|
||||||
|
::ev/origin (if direct-invitation?
|
||||||
|
"organization-invitation-acceptance"
|
||||||
|
"team-invitation-acceptance")))))))
|
||||||
|
|
||||||
(case state
|
(case state
|
||||||
:created
|
:created
|
||||||
|
|||||||
@ -121,31 +121,72 @@
|
|||||||
(t/is (nil? (:days-since-member-added event)))))))
|
(t/is (nil? (:days-since-member-added event)))))))
|
||||||
|
|
||||||
(t/deftest accept-organization-invitation-audit-event-test
|
(t/deftest accept-organization-invitation-audit-event-test
|
||||||
(let [emitted (atom [])
|
(let [emitted (atom [])]
|
||||||
props {:team-id "team-1"
|
|
||||||
:organization-id "organization-1"
|
|
||||||
:role :editor
|
|
||||||
:invitation-id "invitation-1"
|
|
||||||
:organization-member-add-source "team-invitation"
|
|
||||||
:belongs-to-team-on-add true
|
|
||||||
:organization-member-count-before 4}]
|
|
||||||
(with-redefs [st/emit! (fn
|
(with-redefs [st/emit! (fn
|
||||||
([event]
|
([event]
|
||||||
(swap! emitted conj event))
|
(swap! emitted conj event))
|
||||||
([event & events]
|
([event & events]
|
||||||
(swap! emitted into (cons event events))))]
|
(swap! emitted into (cons event events))))]
|
||||||
(verify-token/handle-token
|
(t/testing "accepting a team invitation that adds an organization member"
|
||||||
{:iss :team-invitation
|
(verify-token/handle-token
|
||||||
:state :created
|
{:iss :team-invitation
|
||||||
:team-id "team-1"
|
:state :created
|
||||||
:organization-invitation-audit
|
:team-id "team-1"
|
||||||
{:origin "team-invitation-acceptance"
|
:organization-id "organization-1"
|
||||||
:props props}}))
|
:role :editor
|
||||||
|
:invitation-id "invitation-1"
|
||||||
|
:member-id "invitee-1"
|
||||||
|
:profile-id "inviter-1"
|
||||||
|
:organization-member-count-before 4})
|
||||||
|
|
||||||
(let [event @(first @emitted)]
|
(t/is (= {::ev/name "accept-organization-invitation"
|
||||||
(t/is (= "accept-organization-invitation" (::ev/name event)))
|
::ev/origin "team-invitation-acceptance"
|
||||||
(t/is (= "team-invitation-acceptance" (::ev/origin event)))
|
:team-id "team-1"
|
||||||
(t/is (= props (dissoc event ::ev/name ::ev/origin))))))
|
:organization-id "organization-1"
|
||||||
|
:role :editor
|
||||||
|
:invitation-id "invitation-1"
|
||||||
|
:user-id "invitee-1"
|
||||||
|
:user-who-send-invitation "inviter-1"
|
||||||
|
:organization-member-add-source "team-invitation"
|
||||||
|
:belongs-to-team-on-add true
|
||||||
|
:organization-member-count-before 4}
|
||||||
|
@(first @emitted))))
|
||||||
|
|
||||||
|
(reset! emitted [])
|
||||||
|
(t/testing "accepting an invitation directly to the organization"
|
||||||
|
(verify-token/handle-token
|
||||||
|
{:iss :team-invitation
|
||||||
|
:state :created
|
||||||
|
:organization-id "organization-2"
|
||||||
|
:organization-team-id "team-default"
|
||||||
|
:role :viewer
|
||||||
|
:invitation-id "invitation-2"
|
||||||
|
:member-id "invitee-2"
|
||||||
|
:profile-id "inviter-2"
|
||||||
|
:organization-member-count-before 0})
|
||||||
|
|
||||||
|
(t/is (= {::ev/name "accept-organization-invitation"
|
||||||
|
::ev/origin "organization-invitation-acceptance"
|
||||||
|
:organization-id "organization-2"
|
||||||
|
:role :viewer
|
||||||
|
:invitation-id "invitation-2"
|
||||||
|
:user-id "invitee-2"
|
||||||
|
:user-who-send-invitation "inviter-2"
|
||||||
|
:organization-member-add-source "direct-organization-invitation"
|
||||||
|
:belongs-to-team-on-add false
|
||||||
|
:organization-member-count-before 0}
|
||||||
|
@(first @emitted))))
|
||||||
|
|
||||||
|
(reset! emitted [])
|
||||||
|
(t/testing "does not audit a team invitation for an existing organization member"
|
||||||
|
(verify-token/handle-token
|
||||||
|
{:iss :team-invitation
|
||||||
|
:state :created
|
||||||
|
:team-id "team-3"
|
||||||
|
:organization-id "organization-3"
|
||||||
|
:role :editor})
|
||||||
|
|
||||||
|
(t/is (= 3 (count @emitted)))))))
|
||||||
|
|
||||||
(t/deftest build-admin-console-url-preserves-public-uri-subpath
|
(t/deftest build-admin-console-url-preserves-public-uri-subpath
|
||||||
(t/testing "builds admin console routes below the configured Penpot subpath"
|
(t/testing "builds admin console routes below the configured Penpot subpath"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user