mirror of
https://github.com/penpot/penpot.git
synced 2026-06-01 05:00:17 +00:00
714 lines
28 KiB
Clojure
714 lines
28 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns app.rpc.commands.teams-invitations
|
|
(:require
|
|
[app.binfile.common :as bfc]
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.exceptions :as ex]
|
|
[app.common.features :as cfeat]
|
|
[app.common.logging :as l]
|
|
[app.common.schema :as sm]
|
|
[app.common.time :as ct]
|
|
[app.common.types.team :as types.team]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.db :as db]
|
|
[app.email :as eml]
|
|
[app.email.blacklist :as email.blacklist]
|
|
[app.loggers.audit :as audit]
|
|
[app.main :as-alias main]
|
|
[app.nitrate :as nitrate]
|
|
[app.rpc :as-alias rpc]
|
|
[app.rpc.commands.profile :as profile]
|
|
[app.rpc.commands.teams :as teams]
|
|
[app.rpc.doc :as-alias doc]
|
|
[app.rpc.helpers :as rph]
|
|
[app.rpc.quotes :as quotes]
|
|
[app.setup :as-alias setup]
|
|
[app.tokens :as tokens]
|
|
[app.util.services :as sv]
|
|
[cuerdas.core :as str]))
|
|
|
|
;; --- Mutation: Create Team Invitation
|
|
|
|
(def sql:upsert-team-invitation
|
|
"insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until)
|
|
values (?, ?, null, ?, ?, ?, ?)
|
|
on conflict(team_id, email_to) do
|
|
update set role = ?, valid_until = ?, updated_at = now()
|
|
returning *")
|
|
|
|
(def sql:upsert-org-invitation
|
|
"insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until)
|
|
values (?, null, ?, ?, ?, ?, ?)
|
|
on conflict(org_id, email_to) where team_id is null do
|
|
update set role = ?, valid_until = ?, updated_at = now()
|
|
returning *")
|
|
|
|
(defn- create-invitation-token
|
|
[cfg {:keys [profile-id valid-until organization-id organization-name team-id member-id member-email role]}]
|
|
(tokens/generate cfg
|
|
{:iss :team-invitation
|
|
:exp valid-until
|
|
:profile-id profile-id
|
|
:role role
|
|
:team-id team-id
|
|
:organization-id organization-id
|
|
:organization-name organization-name
|
|
:member-email member-email
|
|
:member-id member-id}))
|
|
|
|
(defn- create-profile-identity-token
|
|
[cfg profile-id]
|
|
(assert (uuid? profile-id) "expected valid uuid for profile-id")
|
|
(tokens/generate cfg
|
|
{:iss :profile-identity
|
|
:profile-id profile-id
|
|
:exp (ct/in-future {:days 30})}))
|
|
|
|
(def ^:private schema:create-invitation
|
|
[:map {:title "params:create-invitation"}
|
|
[::rpc/profile-id ::sm/uuid]
|
|
[:team
|
|
[:map
|
|
[:id ::sm/uuid]
|
|
[:name :string]]]
|
|
[:profile
|
|
[:map
|
|
[:id ::sm/uuid]
|
|
[:fullname :string]]]
|
|
[:role types.team/schema:role]
|
|
[:email ::sm/email]])
|
|
|
|
(def ^:private schema:create-org-invitation
|
|
[:map {:title "params:create-org-invitation"}
|
|
[::rpc/profile-id ::sm/uuid]
|
|
[:organization
|
|
[:map
|
|
[:id ::sm/uuid]
|
|
[:name :string]
|
|
[:initials [:maybe :string]]
|
|
[:logo ::sm/uri]]]
|
|
[:profile
|
|
[:map
|
|
[:id ::sm/uuid]
|
|
[:fullname :string]]]
|
|
[:role types.team/schema:role]
|
|
[:email ::sm/email]])
|
|
|
|
(def ^:private check-create-invitation-params
|
|
(sm/check-fn schema:create-invitation))
|
|
|
|
(def ^:private check-create-org-invitation-params
|
|
(sm/check-fn schema:create-org-invitation))
|
|
|
|
(defn- allow-invitation-emails?
|
|
[member]
|
|
(let [notifications (dm/get-in member [:props :notifications])]
|
|
(not= :none (:email-invites notifications))))
|
|
|
|
(defn- create-invitation
|
|
[{:keys [::db/conn] :as cfg} {:keys [team organization profile role email] :as params}]
|
|
|
|
(assert (db/connection-map? cfg)
|
|
"expected cfg with valid connection")
|
|
(if organization
|
|
(assert (check-create-org-invitation-params params))
|
|
(assert (check-create-invitation-params params)))
|
|
|
|
(let [email (profile/clean-email email)
|
|
member (profile/get-profile-by-email conn email)]
|
|
|
|
(when (and (email.blacklist/enabled? cfg)
|
|
(email.blacklist/contains? cfg email))
|
|
(ex/raise :type :restriction
|
|
:code :email-domain-is-not-allowed
|
|
:hint "email domain is in the blacklist"))
|
|
|
|
;; When we have email verification disabled and invitation user is
|
|
;; already present in the database, we proceed to add it to the
|
|
;; team as-is, without email roundtrip.
|
|
|
|
;; TODO: if member does not exists and email verification is
|
|
;; disabled, we should proceed to create the profile (?)
|
|
(if (and (not (contains? cf/flags :email-verification))
|
|
(some? member))
|
|
(let [params (merge {:team-id (:id team)
|
|
:profile-id (:id member)}
|
|
(get types.team/permissions-for-role role))]
|
|
|
|
(if organization
|
|
;; Insert the invited member to the org
|
|
(when (contains? cf/flags :nitrate)
|
|
(teams/initialize-user-in-nitrate-org cfg (:id member) (:id organization) email))
|
|
;; Insert the invited member to the team
|
|
(teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}))
|
|
|
|
;; If profile is not yet verified, mark it as verified because
|
|
;; accepting an invitation link serves as verification.
|
|
(when-not (:is-active member)
|
|
(db/update! conn :profile
|
|
{:is-active true}
|
|
{:id (:id member)}))
|
|
|
|
nil)
|
|
|
|
(do
|
|
(some->> member (teams/check-profile-muted conn))
|
|
(teams/check-email-bounce conn email true)
|
|
(teams/check-email-spam conn email true)
|
|
|
|
(let [id (uuid/next)
|
|
expire (if organization
|
|
(ct/in-future "876000h") ;; Organization invitations doesn't expire
|
|
(ct/in-future "168h")) ;; 7 days
|
|
invitation (db/exec-one! conn (if organization
|
|
[sql:upsert-org-invitation id
|
|
(:id organization)
|
|
(str/lower email)
|
|
(:id profile)
|
|
(name role) expire
|
|
(name role) expire]
|
|
[sql:upsert-team-invitation id
|
|
(:id team)
|
|
(str/lower email)
|
|
(:id profile)
|
|
(name role) expire
|
|
(name role) expire]))
|
|
updated? (not= id (:id invitation))
|
|
profile-id (:id profile)
|
|
tprops {:profile-id profile-id
|
|
:invitation-id (:id invitation)
|
|
:valid-until expire
|
|
:team-id (:id team)
|
|
:organization-id (:id organization)
|
|
:organization-name (:name organization)
|
|
:member-email (:email-to invitation)
|
|
:member-id (:id member)
|
|
:role role}
|
|
itoken (create-invitation-token cfg tprops)
|
|
ptoken (create-profile-identity-token cfg profile-id)]
|
|
|
|
(when (contains? cf/flags :log-invitation-tokens)
|
|
(l/info :hint "invitation token" :token itoken))
|
|
|
|
(let [props (-> (dissoc tprops :profile-id)
|
|
(audit/clean-props))
|
|
evname (cond
|
|
(and updated? organization) "update-org-invitation"
|
|
updated? "update-team-invitation"
|
|
organization "create-org-invitation"
|
|
:else "create-team-invitation")
|
|
event (-> (audit/event-from-rpc-params params)
|
|
(assoc :name evname)
|
|
(assoc :props props))]
|
|
(audit/submit cfg event))
|
|
|
|
(when (allow-invitation-emails? member)
|
|
(if organization
|
|
(when (contains? cf/flags :nitrate)
|
|
(eml/send! {::eml/conn conn
|
|
::eml/factory eml/invite-to-org
|
|
:public-uri (cf/get :public-uri)
|
|
:to email
|
|
:invited-by (:fullname profile)
|
|
:user-name (:fullname member)
|
|
:organization-name (:name organization)
|
|
:organization-logo (:logo organization)
|
|
:organization-initials (:initials organization)
|
|
:token itoken
|
|
:extra-data ptoken}))
|
|
(let [team (if (contains? cf/flags :nitrate)
|
|
(nitrate/add-org-info-to-team cfg team {})
|
|
team)]
|
|
(eml/send! {::eml/conn conn
|
|
::eml/factory eml/invite-to-team
|
|
:public-uri (cf/get :public-uri)
|
|
:to email
|
|
:invited-by (:fullname profile)
|
|
:team (:name team)
|
|
:organization (dm/get-in team [:organization :name])
|
|
:token itoken
|
|
:extra-data ptoken}))))
|
|
|
|
itoken)))))
|
|
|
|
(defn create-org-invitation
|
|
[cfg {:keys [::rpc/profile-id id name initials logo] :as params}]
|
|
(let [profile (db/get-by-id cfg :profile profile-id)]
|
|
(create-invitation cfg
|
|
(assoc params
|
|
:organization {:id id :name name :initials initials :logo logo}
|
|
:profile profile
|
|
:role :editor))))
|
|
|
|
(defn- add-member-to-team
|
|
[{:keys [::db/conn] :as cfg} profile team role member]
|
|
(assert (db/connection-map? cfg)
|
|
"expected cfg with valid connection")
|
|
|
|
(let [team-id (:id team)
|
|
params (merge
|
|
{:team-id team-id
|
|
:profile-id (:id member)}
|
|
(get types.team/permissions-for-role role))]
|
|
|
|
;; Do not allow blocked users to join teams.
|
|
(when (:is-blocked member)
|
|
(ex/raise :type :restriction
|
|
:code :profile-blocked))
|
|
|
|
(quotes/check!
|
|
{::db/conn conn
|
|
::quotes/id ::quotes/profiles-per-team
|
|
::quotes/profile-id (:id member)
|
|
::quotes/team-id team-id})
|
|
|
|
;; Insert the member to the team
|
|
(teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})
|
|
|
|
;; Delete any request
|
|
(db/delete! conn :team-access-request
|
|
{:team-id team-id :requester-id (:id member)})
|
|
|
|
;; Delete any invitation
|
|
(db/delete! conn :team-invitation
|
|
{:team-id team-id :email-to (:email member)})
|
|
|
|
(eml/send! {::eml/conn conn
|
|
::eml/factory eml/join-team
|
|
:public-uri (cf/get :public-uri)
|
|
:to (:email member)
|
|
:invited-by (:fullname profile)
|
|
:team (:name team)
|
|
:team-id (:id team)})))
|
|
|
|
(def ^:private sql:valid-access-request-profiles
|
|
"SELECT p.id, p.email, p.is_blocked
|
|
FROM team_access_request AS tr
|
|
JOIN profile AS p ON (tr.requester_id = p.id)
|
|
WHERE tr.team_id = ?
|
|
AND tr.auto_join_until > now()
|
|
AND (p.deleted_at IS NULL OR
|
|
p.deleted_at > now())")
|
|
|
|
(defn- get-valid-access-request-profiles
|
|
[conn team-id]
|
|
(db/exec! conn [sql:valid-access-request-profiles team-id]))
|
|
|
|
(def ^:private xf:map-email (map :email))
|
|
|
|
(defn- create-team-invitations
|
|
"Unified function to handle both create and resend team invitations.
|
|
Accepts either:
|
|
- emails (set) + role (single role for all emails)
|
|
- invitations (vector of {:email :role} maps)"
|
|
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails invitations] :as params}]
|
|
(let [;; Normalize input to a consistent format: [{:email :role}]
|
|
invitation-data (cond
|
|
;; Case 1: emails + single role (create invitations style)
|
|
(and emails role)
|
|
(map (fn [email] {:email email :role role}) emails)
|
|
|
|
;; Case 2: invitations with individual roles (resend invitations style)
|
|
(some? invitations)
|
|
invitations
|
|
|
|
:else
|
|
(throw (ex-info "Invalid parameters: must provide either emails+role or invitations" {})))
|
|
|
|
invitation-emails (into #{} (map :email) invitation-data)
|
|
|
|
join-requests (->> (get-valid-access-request-profiles conn (:id team))
|
|
(d/index-by :email))
|
|
|
|
team-members (into #{} xf:map-email
|
|
(teams/get-team-members conn (:id team)))
|
|
|
|
invitations (into #{}
|
|
(comp
|
|
;; We don't re-send invitations to
|
|
;; already existing members
|
|
(remove #(contains? team-members (:email %)))
|
|
;; We don't send invitations to
|
|
;; join-requested members
|
|
(remove #(contains? join-requests (:email %)))
|
|
(map (fn [{:keys [email role]}]
|
|
(create-invitation cfg
|
|
(-> params
|
|
(assoc :email email)
|
|
(assoc :role role)))))
|
|
(remove nil?))
|
|
invitation-data)]
|
|
|
|
;; For requested invitations, do not send invitation emails, add
|
|
;; the user directly to the team
|
|
(->> join-requests
|
|
(filter #(contains? invitation-emails (key %)))
|
|
(map (fn [[email member]]
|
|
(let [role (:role (first (filter #(= (:email %) email) invitation-data)))]
|
|
(add-member-to-team cfg profile team role member))))
|
|
(doall))
|
|
|
|
invitations))
|
|
|
|
(def ^:private schema:create-team-invitations
|
|
[:and
|
|
[:map {:title "create-team-invitations"}
|
|
[:team-id ::sm/uuid]
|
|
;; Support both formats:
|
|
;; 1. emails (set) + role (single role for all)
|
|
;; 2. invitations (vector of {:email :role} maps)
|
|
[:emails {:optional true} [::sm/set ::sm/email]]
|
|
[:role {:optional true} types.team/schema:role]
|
|
[:invitations {:optional true} [:vector [:map
|
|
[:email ::sm/email]
|
|
[:role types.team/schema:role]]]]]
|
|
|
|
;; Ensure exactly one format is provided
|
|
[:fn (fn [params]
|
|
(let [has-emails-role (and (contains? params :emails)
|
|
(contains? params :role))
|
|
has-invitations (contains? params :invitations)]
|
|
(and (or has-emails-role has-invitations)
|
|
(not (and has-emails-role has-invitations)))))]])
|
|
|
|
(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 allows to send single or multiple invitations to join the team.
|
|
|
|
Supports two parameter formats:
|
|
1. emails (set) + role (single role for all emails)
|
|
2. invitations (vector of {:email :role} maps for individual roles)"
|
|
{::doc/added "1.17"
|
|
::doc/module :teams
|
|
::sm/params schema:create-team-invitations}
|
|
[cfg {:keys [::rpc/profile-id team-id role emails] :as params}]
|
|
(let [perms (teams/get-permissions cfg profile-id team-id)
|
|
profile (db/get-by-id cfg :profile profile-id)
|
|
;; Determine which format is being used
|
|
using-emails-format? (and emails role)
|
|
;; Handle both parameter formats
|
|
emails (if using-emails-format?
|
|
(into #{} (map profile/clean-email) emails)
|
|
#{})
|
|
;; Calculate total invitation count for both formats
|
|
invitation-count (if using-emails-format?
|
|
(count emails)
|
|
(count (:invitations params)))]
|
|
|
|
(when-not (:is-admin perms)
|
|
(ex/raise :type :validation
|
|
:code :insufficient-permissions))
|
|
|
|
(when (> invitation-count 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))
|
|
|
|
(-> cfg
|
|
(assoc ::quotes/profile-id profile-id)
|
|
(assoc ::quotes/team-id team-id)
|
|
(assoc ::quotes/incr invitation-count)
|
|
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
|
|
{::quotes/id ::quotes/profiles-per-team}))
|
|
|
|
;; Check if the current profile is allowed to send emails
|
|
(teams/check-profile-muted cfg profile)
|
|
|
|
(let [team (db/get-by-id cfg :team team-id)
|
|
;; NOTE: Is important pass RPC method params down to the
|
|
;; `create-team-invitations` because it uses the implicit
|
|
;; RPC properties from params for fill necessary data on
|
|
;; emiting an entry to the audit-log
|
|
invitations (db/tx-run! cfg create-team-invitations
|
|
(-> params
|
|
(assoc :profile profile)
|
|
(assoc :team team)
|
|
;; Pass parameters in the correct format for the unified function
|
|
(cond-> using-emails-format?
|
|
;; If using emails+role format, ensure both are present
|
|
(assoc :emails emails :role role)
|
|
;; If using invitations format, the :invitations key is already in params
|
|
(not using-emails-format?) identity)))]
|
|
|
|
(with-meta {:total (count invitations)
|
|
:invitations invitations}
|
|
{::audit/props {:invitations (count invitations)}}))))
|
|
|
|
;; --- Mutation: Create Team & Invite Members
|
|
|
|
(def ^:private schema:create-team-with-invitations
|
|
[:map {:title "create-team-with-invitations"}
|
|
[:name [:string {:max 250}]]
|
|
[:features {:optional true} ::cfeat/features]
|
|
[:id {:optional true} ::sm/uuid]
|
|
[:emails [::sm/set ::sm/email]]
|
|
[:role types.team/schema:role]])
|
|
|
|
(sv/defmethod ::create-team-with-invitations
|
|
{::doc/added "1.17"
|
|
::doc/module :teams
|
|
::sm/params schema:create-team-with-invitations
|
|
::db/transaction true}
|
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
|
|
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
|
(cfeat/check-client-features! (:features params)))
|
|
|
|
params (-> params
|
|
(assoc :profile-id profile-id)
|
|
(assoc :features features))
|
|
|
|
team (teams/create-team cfg params)
|
|
emails (into #{} (map profile/clean-email) emails)]
|
|
|
|
(-> cfg
|
|
(assoc ::quotes/profile-id profile-id)
|
|
(assoc ::quotes/team-id (:id team))
|
|
(assoc ::quotes/incr (count emails))
|
|
(quotes/check! {::quotes/id ::quotes/teams-per-profile}
|
|
{::quotes/id ::quotes/invitations-per-team}
|
|
{::quotes/id ::quotes/profiles-per-team}))
|
|
|
|
(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 :name "create-team")
|
|
(assoc :props props))]
|
|
(audit/submit cfg event))
|
|
|
|
;; Create invitations for all provided emails.
|
|
(let [profile (db/get-by-id conn :profile profile-id)
|
|
params (-> params
|
|
(assoc :team team)
|
|
(assoc :profile profile)
|
|
(assoc :role role))
|
|
invitations (->> emails
|
|
(map (fn [email] (assoc params :email email)))
|
|
(map (partial create-invitation cfg)))]
|
|
|
|
(vary-meta team assoc ::audit/props {:invitations (count invitations)}))))
|
|
|
|
;; --- Query: get-team-invitation-token
|
|
|
|
(def ^:private schema:get-team-invitation-token
|
|
[:map {:title "get-team-invitation-token"}
|
|
[:team-id ::sm/uuid]
|
|
[:email ::sm/email]])
|
|
|
|
(sv/defmethod ::get-team-invitation-token
|
|
{::doc/added "1.17"
|
|
::doc/module :teams
|
|
::sm/params schema:get-team-invitation-token}
|
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
|
|
(teams/check-read-permissions! pool profile-id team-id)
|
|
(let [email (profile/clean-email email)
|
|
invit (-> (db/get pool :team-invitation
|
|
{:team-id team-id
|
|
:email-to email})
|
|
(update :role keyword))
|
|
|
|
member (profile/get-profile-by-email pool (:email-to invit))
|
|
token (create-invitation-token cfg {:team-id (:team-id invit)
|
|
:profile-id profile-id
|
|
:valid-until (:valid-until invit)
|
|
:role (:role invit)
|
|
:member-id (:id member)
|
|
:member-email (or (:email member)
|
|
(profile/clean-email (:email-to invit)))})]
|
|
{:token token}))
|
|
|
|
;; --- Mutation: Update invitation role
|
|
|
|
(def ^:private schema:update-team-invitation-role
|
|
[:map {:title "update-team-invitation-role"}
|
|
[:team-id ::sm/uuid]
|
|
[:email ::sm/email]
|
|
[:role types.team/schema:role]])
|
|
|
|
(sv/defmethod ::update-team-invitation-role
|
|
{::doc/added "1.17"
|
|
::doc/module :teams
|
|
::sm/params schema:update-team-invitation-role
|
|
::db/transaction true}
|
|
[{:keys [::db/conn]} {:keys [::rpc/profile-id team-id email role] :as params}]
|
|
(let [perms (teams/get-permissions conn profile-id team-id)]
|
|
|
|
(when-not (:is-admin perms)
|
|
(ex/raise :type :validation
|
|
:code :insufficient-permissions))
|
|
|
|
(db/update! conn :team-invitation
|
|
{:role (name role) :updated-at (ct/now)}
|
|
{:team-id team-id :email-to (profile/clean-email email)})
|
|
|
|
nil))
|
|
|
|
;; --- Mutation: Delete invitation
|
|
|
|
(def ^:private schema:delete-team-invition
|
|
[:map {:title "delete-team-invitation"}
|
|
[:team-id ::sm/uuid]
|
|
[:email ::sm/email]])
|
|
|
|
(sv/defmethod ::delete-team-invitation
|
|
{::doc/added "1.17"
|
|
::sm/params schema:delete-team-invition
|
|
::db/transaction true}
|
|
[{:keys [::db/conn]} {:keys [::rpc/profile-id team-id email] :as params}]
|
|
(let [perms (teams/get-permissions conn profile-id team-id)]
|
|
|
|
(when-not (:is-admin perms)
|
|
(ex/raise :type :validation
|
|
:code :insufficient-permissions))
|
|
|
|
(let [invitation (db/delete! conn :team-invitation
|
|
{:team-id team-id
|
|
:email-to (profile/clean-email email)}
|
|
{::db/return-keys true})]
|
|
(rph/wrap nil {::audit/props {:invitation-id (:id invitation)}}))))
|
|
|
|
|
|
;; --- Mutation: Request Team Invitation
|
|
|
|
(def ^:private sql:get-team-owner
|
|
"SELECT p.*
|
|
FROM profile AS p
|
|
JOIN team_profile_rel AS tpr ON (tpr.profile_id = p.id)
|
|
WHERE tpr.team_id = ?
|
|
AND tpr.is_owner IS TRUE")
|
|
|
|
(defn- get-team-owner
|
|
"Return a complete profile of the team owner"
|
|
[conn team-id]
|
|
(->> (db/exec! conn [sql:get-team-owner team-id])
|
|
(remove db/is-row-deleted?)
|
|
(map profile/decode-row)
|
|
(first)))
|
|
|
|
(defn- check-existing-team-access-request
|
|
"Checks if an existing team access request is still valid"
|
|
[{:keys [::db/conn]} team-id profile-id]
|
|
(when-let [request (db/get* conn :team-access-request
|
|
{:team-id team-id
|
|
:requester-id profile-id})]
|
|
(when (ct/is-after? (:valid-until request) (ct/now))
|
|
(ex/raise :type :validation
|
|
:code :request-already-sent
|
|
:hint "you have already made a request to join this team less than 24 hours ago"))))
|
|
|
|
(def ^:private sql:upsert-team-access-request
|
|
"INSERT INTO team_access_request (id, team_id, requester_id, valid_until, auto_join_until)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT (team_id, requester_id)
|
|
DO UPDATE SET valid_until = ?, auto_join_until = ?, updated_at = now()
|
|
RETURNING *")
|
|
|
|
(defn- upsert-team-access-request
|
|
"Create or update team access request for provided team and profile-id"
|
|
[{:keys [::db/conn] :as cfg} team-id requester-id]
|
|
(check-existing-team-access-request cfg team-id requester-id)
|
|
(let [valid-until (ct/in-future {:hours 24})
|
|
auto-join-until (ct/in-future {:days 7})
|
|
request-id (uuid/next)]
|
|
(db/exec-one! conn [sql:upsert-team-access-request
|
|
request-id team-id requester-id
|
|
valid-until auto-join-until
|
|
valid-until auto-join-until])))
|
|
|
|
(defn- get-file-for-team-access-request
|
|
"A specific method for obtain a file with name and page-id used for
|
|
team request access procediment"
|
|
[cfg file-id]
|
|
(let [file (bfc/get-file cfg file-id :migrate? false)]
|
|
(-> file
|
|
(dissoc :data)
|
|
(dissoc :deleted-at)
|
|
(assoc :page-id (-> file :data :pages first)))))
|
|
|
|
(def ^:private schema:create-team-access-request
|
|
[:and
|
|
[:map {:title "create-team-access-request"}
|
|
[:file-id {:optional true} ::sm/uuid]
|
|
[:team-id {:optional true} ::sm/uuid]
|
|
[:is-viewer {:optional true} ::sm/boolean]]
|
|
|
|
[:fn (fn [params]
|
|
(or (contains? params :file-id)
|
|
(contains? params :team-id)))]])
|
|
|
|
(sv/defmethod ::create-team-access-request
|
|
"A rpc call that allow to request for an invitations to join the team."
|
|
{::doc/added "2.2.0"
|
|
::doc/module :teams
|
|
::sm/params schema:create-team-access-request
|
|
::db/transaction true}
|
|
[{:keys [::db/conn] :as cfg}
|
|
{:keys [::rpc/profile-id file-id team-id is-viewer] :as params}]
|
|
|
|
(let [requester (profile/get-profile conn profile-id)
|
|
team (if team-id
|
|
(->> (db/get-by-id conn :team team-id)
|
|
(teams/decode-row))
|
|
(teams/get-team-for-file conn file-id))
|
|
|
|
team-id (:id team)
|
|
|
|
team-owner (get-team-owner conn team-id)
|
|
|
|
file (when (some? file-id)
|
|
(get-file-for-team-access-request cfg file-id))]
|
|
|
|
(-> cfg
|
|
(assoc ::quotes/profile-id profile-id)
|
|
(assoc ::quotes/team-id team-id)
|
|
(quotes/check! {::quotes/id ::quotes/team-access-requests-per-team}
|
|
{::quotes/id ::quotes/team-access-requests-per-requester}))
|
|
|
|
(teams/check-profile-muted conn requester)
|
|
(teams/check-email-bounce conn (:email team-owner) false)
|
|
(teams/check-email-spam conn (:email team-owner) true)
|
|
|
|
(let [request (upsert-team-access-request cfg team-id profile-id)
|
|
factory (cond
|
|
(and (some? file) (:is-default team) is-viewer)
|
|
eml/request-file-access-yourpenpot-view
|
|
|
|
(and (some? file) (:is-default team))
|
|
eml/request-file-access-yourpenpot
|
|
|
|
(some? file)
|
|
eml/request-file-access
|
|
|
|
:else
|
|
eml/request-team-access)]
|
|
|
|
(eml/send! {::eml/conn conn
|
|
::eml/factory factory
|
|
:public-uri (cf/get :public-uri)
|
|
:to (:email team-owner)
|
|
:requested-by (:fullname requester)
|
|
:requested-by-email (:email requester)
|
|
:team-name (:name team)
|
|
:team-id team-id
|
|
:file-name (:name file)
|
|
:file-id file-id
|
|
:page-id (:page-id file)})
|
|
|
|
(with-meta {:request request}
|
|
{::audit/props {:request 1}}))))
|