0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-25 14:11:33 -05:00

🎉 Add team invitations API

This commit is contained in:
Pablo Alba 2022-02-15 14:18:45 +01:00 committed by Andrey Antukh
parent 09a4cb30ec
commit 1990232adc
6 changed files with 155 additions and 10 deletions

View file

@ -205,9 +205,12 @@
{:name "0065-add-trivial-spelling-fixes"
:fn (mg/resource "app/migrations/sql/0065-add-trivial-spelling-fixes.sql")}
{:name "0066-add-frame-thumbnail-table"
:fn (mg/resource "app/migrations/sql/0066-add-frame-thumbnail-table.sql")}
{:name "0067-add-team-invitation-table"
:fn (mg/resource "app/migrations/sql/0067-add-team-invitation-table.sql")}
])

View file

@ -0,0 +1,15 @@
CREATE TABLE team_invitation (
team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
email_to text NOT NULL,
role text NOT NULL,
valid_until timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY(team_id, email_to)
);
ALTER TABLE team_invitation
ALTER COLUMN email_to SET STORAGE external,
ALTER COLUMN role SET STORAGE external;

View file

@ -23,6 +23,7 @@
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.core :as fs]))
;; --- Helpers & Specs
@ -359,12 +360,19 @@
:role role))
nil)))
(def sql:upsert-team-invitation
"insert into team_invitation(team_id, email_to, role, valid_until)
values (?, ?, ?, ?)
on conflict(team_id, email_to) do
update set role = ?, valid_until = ?, updated_at = now();")
(defn- create-team-invitation
[{:keys [conn tokens team profile role email] :as cfg}]
(let [member (profile/retrieve-profile-data-by-email conn email)
token-exp (dt/in-future "48h")
itoken (tokens :generate
{:iss :team-invitation
:exp (dt/in-future "48h")
:exp token-exp
:profile-id (:id profile)
:role role
:team-id (:id team)
@ -386,6 +394,10 @@
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(db/exec-one! conn [sql:upsert-team-invitation
(:id team) (str/lower email) (name role) token-exp (name role) token-exp])
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
:public-uri (:public-uri cfg)
@ -418,3 +430,24 @@
:email email
:role role)))
team)))
;; --- Mutation: Update invitation role
(s/def ::update-team-invitation-role
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
(sv/defmethod ::update-team-invitation-role
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
(db/with-atomic [conn pool]
(let [perms (teams/get-permissions conn profile-id team-id)
team (db/get-by-id conn :team team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(db/update! conn :team-invitation
{:role (name role) :updated-at (dt/now)}
{:team-id (:id team) :email-to (str/lower email)})
nil)))

View file

@ -13,8 +13,9 @@
[app.metrics :as mtx]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
[app.util.services :as sv]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(defmulti process-token (fn [_ _ claims] (:iss claims)))
@ -100,11 +101,18 @@
:opt-un [:internal.tokens.team-invitation/member-id]))
(defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [member-id team-id role] :as claims}]
(let [params (merge {:team-id team-id
[{:keys [conn] :as cfg} {:keys [member-id team-id role member-email] :as claims}]
(let [
member (profile/retrieve-profile conn member-id)
invitation (db/get-by-params conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)}
{:check-not-found false})
;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role)
params (merge {:team-id team-id
:profile-id member-id}
(teams/role->params role))
member (profile/retrieve-profile conn member-id)]
]
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
@ -115,7 +123,12 @@
(db/update! conn :profile
{:is-active true}
{:id member-id}))
(assoc member :is-active true)))
(assoc member :is-active true)
;; Delete the invitation
(db/delete! conn :team-invitation
{:team-id team-id :email-to (str/lower member-email)})))
(defmethod process-token :team-invitation
[{:keys [session] :as cfg} {:keys [profile-id token]} {:keys [member-id] :as claims}]

View file

@ -229,3 +229,20 @@
(defn retrieve-team-stats
[conn team-id]
(db/exec-one! conn [sql:team-stats team-id team-id]))
;; --- Query: Team invitations
(s/def ::team-id ::us/uuid)
(s/def ::team-invitations
(s/keys :req-un [::profile-id ::team-id]))
(def sql:team-invitations
"select email_to as email, role, (valid_until < now()) as expired from team_invitation where team_id = ?")
(sv/defmethod ::team-invitations
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(db/exec! conn [sql:team-invitations team-id])))

View file

@ -35,12 +35,19 @@
;; invite external user without complaints
(let [data (assoc data :email "foo@bar.com")
out (th/mutation! data)]
out (th/mutation! data)
;;retrieve the value from the database and check its content
invitation (db/exec-one!
th/*pool*
["select count(*) as num from team_invitation where team_id = ? and email_to = ?"
(:team-id data) "foo@bar.com"])]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (= 1 (:call-count (deref mock)))))
(t/is (= 1 (:call-count (deref mock))))
(t/is (= 1 (:num invitation))))
;; invite internal user without complaints
(th/reset-mock! mock)
@ -159,4 +166,61 @@
(t/deftest query-team-invitations
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
data {::th/type :team-invitations
:profile-id (:id prof)
:team-id (:id team)}]
;;insert an entry on the database with an enabled invitation
(db/insert! th/*pool* :team-invitation
{:team-id (:team-id data)
:email-to "test1@mail.com"
:role "editor"
:valid-until (dt/in-future "48h")})
;;insert an entry on the database with an expired invitation
(db/insert! th/*pool* :team-invitation
{:team-id (:team-id data)
:email-to "test2@mail.com"
:role "editor"
:valid-until (dt/in-past "48h")})
(let [out (th/query! data)]
(t/is (nil? (:error out)))
(let [result (:result out)
one (first result)
two (second result)]
(t/is (= 2 (count result)))
(t/is (= "test1@mail.com" (:email one)))
(t/is (= "test2@mail.com" (:email two)))
(t/is (false? (:expired one)))
(t/is (true? (:expired two)))))))
(t/deftest update-team-invitation-role
(let [prof (th/create-profile* 1 {:is-active true})
team (th/create-team* 1 {:profile-id (:id prof)})
data {::th/type :update-team-invitation-role
:profile-id (:id prof)
:team-id (:id team)
:email "TEST1@mail.com"
:role :admin}]
;;insert an entry on the database with an invitation
(db/insert! th/*pool* :team-invitation
{:team-id (:team-id data)
:email-to "test1@mail.com"
:role "editor"
:valid-until (dt/in-future "48h")})
(let [out (th/mutation! data)
;;retrieve the value from the database and check its content
result (db/get-by-params th/*pool* :team-invitation
{:team-id (:team-id data) :email-to "test1@mail.com"}
{:check-not-found false})]
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))
(t/is (= "admin" (:role result))))))