mirror of
https://github.com/penpot/penpot.git
synced 2025-01-06 14:50:20 -05:00
Merge pull request #5290 from penpot/niwinz-team-request-access-bugfix
🐛 Several bugfixes related to the request-access feature
This commit is contained in:
commit
8ed508012e
18 changed files with 776 additions and 716 deletions
|
@ -250,6 +250,7 @@
|
||||||
'app.rpc.commands.projects
|
'app.rpc.commands.projects
|
||||||
'app.rpc.commands.search
|
'app.rpc.commands.search
|
||||||
'app.rpc.commands.teams
|
'app.rpc.commands.teams
|
||||||
|
'app.rpc.commands.teams-invitations
|
||||||
'app.rpc.commands.verify-token
|
'app.rpc.commands.verify-token
|
||||||
'app.rpc.commands.viewer
|
'app.rpc.commands.viewer
|
||||||
'app.rpc.commands.webhooks)
|
'app.rpc.commands.webhooks)
|
||||||
|
|
|
@ -36,7 +36,8 @@
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]
|
||||||
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
;; --- FEATURES
|
;; --- FEATURES
|
||||||
|
|
||||||
|
@ -245,16 +246,16 @@
|
||||||
file)))
|
file)))
|
||||||
|
|
||||||
(defn get-file
|
(defn get-file
|
||||||
[{:keys [::db/conn] :as cfg} id & {:keys [project-id
|
[{:keys [::db/conn ::wrk/executor] :as cfg} id
|
||||||
migrate?
|
& {:keys [project-id
|
||||||
include-deleted?
|
migrate?
|
||||||
lock-for-update?]
|
include-deleted?
|
||||||
:or {include-deleted? false
|
lock-for-update?]
|
||||||
lock-for-update? false
|
:or {include-deleted? false
|
||||||
migrate? true}}]
|
lock-for-update? false
|
||||||
(dm/assert!
|
migrate? true}}]
|
||||||
"expected cfg with valid connection"
|
|
||||||
(db/connection-map? cfg))
|
(assert (db/connection? conn) "expected cfg with valid connection")
|
||||||
|
|
||||||
(let [params (merge {:id id}
|
(let [params (merge {:id id}
|
||||||
(when (some? project-id)
|
(when (some? project-id)
|
||||||
|
@ -263,8 +264,14 @@
|
||||||
{::db/check-deleted (not include-deleted?)
|
{::db/check-deleted (not include-deleted?)
|
||||||
::db/remove-deleted (not include-deleted?)
|
::db/remove-deleted (not include-deleted?)
|
||||||
::sql/for-update lock-for-update?})
|
::sql/for-update lock-for-update?})
|
||||||
(feat.fdata/resolve-file-data cfg)
|
(feat.fdata/resolve-file-data cfg))
|
||||||
(decode-row))]
|
|
||||||
|
;; NOTE: we perform the file decoding in a separate thread
|
||||||
|
;; because it has heavy and synchronous operations for
|
||||||
|
;; decoding file body that are not very friendly with virtual
|
||||||
|
;; threads.
|
||||||
|
file (px/invoke! executor #(decode-row file))]
|
||||||
|
|
||||||
(if (and migrate? (fmg/need-migration? file))
|
(if (and migrate? (fmg/need-migration? file))
|
||||||
(migrate-file cfg file)
|
(migrate-file cfg file)
|
||||||
file)))
|
file)))
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.features :as cfeat]
|
[app.common.features :as cfeat]
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.types.team :as tt]
|
[app.common.types.team :as tt]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
@ -25,17 +24,13 @@
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
|
||||||
[app.rpc.permissions :as perms]
|
[app.rpc.permissions :as perms]
|
||||||
[app.rpc.quotes :as quotes]
|
[app.rpc.quotes :as quotes]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.tokens :as tokens]
|
|
||||||
[app.util.blob :as blob]
|
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
[app.worker :as wrk]))
|
||||||
[cuerdas.core :as str]))
|
|
||||||
|
|
||||||
;; --- Helpers & Specs
|
;; --- Helpers & Specs
|
||||||
|
|
||||||
|
@ -84,7 +79,9 @@
|
||||||
(cond-> row
|
(cond-> row
|
||||||
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
(some? features) (assoc :features (db/decode-pgarray features #{}))))
|
||||||
|
|
||||||
(defn- check-profile-muted
|
;; FIXME: move
|
||||||
|
|
||||||
|
(defn check-profile-muted
|
||||||
"Check if the member's email is part of the global bounce report"
|
"Check if the member's email is part of the global bounce report"
|
||||||
[conn member]
|
[conn member]
|
||||||
(let [email (profile/clean-email (:email member))]
|
(let [email (profile/clean-email (:email member))]
|
||||||
|
@ -94,7 +91,7 @@
|
||||||
:email email
|
:email email
|
||||||
:hint "the profile has reported repeatedly as spam or has bounces"))))
|
:hint "the profile has reported repeatedly as spam or has bounces"))))
|
||||||
|
|
||||||
(defn- check-email-bounce
|
(defn check-email-bounce
|
||||||
"Check if the email is part of the global complain report"
|
"Check if the email is part of the global complain report"
|
||||||
[conn email show?]
|
[conn email show?]
|
||||||
(when (eml/has-bounce-reports? conn email)
|
(when (eml/has-bounce-reports? conn email)
|
||||||
|
@ -103,7 +100,7 @@
|
||||||
:email (if show? email "private")
|
:email (if show? email "private")
|
||||||
:hint "this email has been repeatedly reported as bounce")))
|
:hint "this email has been repeatedly reported as bounce")))
|
||||||
|
|
||||||
(defn- check-email-spam
|
(defn check-email-spam
|
||||||
"Check if the member email is part of the global complain report"
|
"Check if the member email is part of the global complain report"
|
||||||
[conn email show?]
|
[conn email show?]
|
||||||
(when (eml/has-complaint-reports? conn email)
|
(when (eml/has-complaint-reports? conn email)
|
||||||
|
@ -267,6 +264,8 @@
|
||||||
[:fn #(or (contains? % :team-id)
|
[:fn #(or (contains? % :team-id)
|
||||||
(contains? % :file-id))]])
|
(contains? % :file-id))]])
|
||||||
|
|
||||||
|
;; FIXME: split in two separated requests
|
||||||
|
|
||||||
(sv/defmethod ::get-team-users
|
(sv/defmethod ::get-team-users
|
||||||
"Get team users by team-id or by file-id"
|
"Get team users by team-id or by file-id"
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
@ -304,20 +303,29 @@
|
||||||
inner join project as p on (f.project_id = p.id)
|
inner join project as p on (f.project_id = p.id)
|
||||||
where p.team_id = ?")
|
where p.team_id = ?")
|
||||||
|
|
||||||
(def sql:team-by-file
|
|
||||||
"select p.team_id as id
|
|
||||||
from project as p
|
|
||||||
join file as f on (p.id = f.project_id)
|
|
||||||
where f.id = ?")
|
|
||||||
|
|
||||||
(defn get-users
|
(defn get-users
|
||||||
[conn team-id]
|
[conn team-id]
|
||||||
(db/exec! conn [sql:team-users team-id team-id team-id]))
|
(db/exec! conn [sql:team-users team-id team-id team-id]))
|
||||||
|
|
||||||
|
(def sql:get-team-by-file
|
||||||
|
"SELECT t.*
|
||||||
|
FROM team AS t
|
||||||
|
JOIN project AS p ON (p.team_id = t.id)
|
||||||
|
JOIN file AS f ON (f.project_id = p.id)
|
||||||
|
WHERE f.id = ?")
|
||||||
|
|
||||||
(defn get-team-for-file
|
(defn get-team-for-file
|
||||||
[conn file-id]
|
[conn file-id]
|
||||||
(->> [sql:team-by-file file-id]
|
(let [team (->> (db/exec! conn [sql:get-team-by-file file-id])
|
||||||
(db/exec-one! conn)))
|
(remove db/is-row-deleted?)
|
||||||
|
(map decode-row)
|
||||||
|
(first))]
|
||||||
|
(when-not team
|
||||||
|
(ex/raise :type :not-found
|
||||||
|
:code :object-not-found
|
||||||
|
:hint "database object not found"))
|
||||||
|
|
||||||
|
team))
|
||||||
|
|
||||||
;; --- Query: Team Stats
|
;; --- Query: Team Stats
|
||||||
|
|
||||||
|
@ -505,8 +513,6 @@
|
||||||
|
|
||||||
;; --- Mutation: Leave Team
|
;; --- Mutation: Leave Team
|
||||||
|
|
||||||
(declare role->params)
|
|
||||||
|
|
||||||
(defn leave-team
|
(defn leave-team
|
||||||
[conn {:keys [profile-id id reassign-to]}]
|
[conn {:keys [profile-id id reassign-to]}]
|
||||||
(let [perms (get-permissions conn profile-id id)
|
(let [perms (get-permissions conn profile-id id)
|
||||||
|
@ -536,7 +542,7 @@
|
||||||
|
|
||||||
;; assign owner role to new profile
|
;; assign owner role to new profile
|
||||||
(db/update! conn :team-profile-rel
|
(db/update! conn :team-profile-rel
|
||||||
(role->params :owner)
|
(get tt/permissions-for-role :owner)
|
||||||
{:team-id id :profile-id reassign-to}))
|
{:team-id id :profile-id reassign-to}))
|
||||||
|
|
||||||
;; and finally, if all other conditions does not match and the
|
;; and finally, if all other conditions does not match and the
|
||||||
|
@ -607,16 +613,6 @@
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
;; --- Mutation: Team Update Role
|
;; --- Mutation: Team Update Role
|
||||||
(def schema:role
|
|
||||||
[::sm/one-of tt/valid-roles])
|
|
||||||
|
|
||||||
(defn role->params
|
|
||||||
[role]
|
|
||||||
(case role
|
|
||||||
:admin {:is-owner false :is-admin true :can-edit true}
|
|
||||||
:editor {:is-owner false :is-admin false :can-edit true}
|
|
||||||
:owner {:is-owner true :is-admin true :can-edit true}
|
|
||||||
:viewer {:is-owner false :is-admin false :can-edit false}))
|
|
||||||
|
|
||||||
(defn update-team-member-role
|
(defn update-team-member-role
|
||||||
[{:keys [::db/conn ::mbus/msgbus]} {:keys [profile-id team-id member-id role] :as params}]
|
[{:keys [::db/conn ::mbus/msgbus]} {:keys [profile-id team-id member-id role] :as params}]
|
||||||
|
@ -657,7 +653,7 @@
|
||||||
:team-id team-id
|
:team-id team-id
|
||||||
:role role})
|
:role role})
|
||||||
|
|
||||||
(let [params (role->params role)]
|
(let [params (get tt/permissions-for-role role)]
|
||||||
;; Only allow single owner on team
|
;; Only allow single owner on team
|
||||||
(when (= role :owner)
|
(when (= role :owner)
|
||||||
(db/update! conn :team-profile-rel
|
(db/update! conn :team-profile-rel
|
||||||
|
@ -675,7 +671,7 @@
|
||||||
[:map {:title "update-team-member-role"}
|
[:map {:title "update-team-member-role"}
|
||||||
[:team-id ::sm/uuid]
|
[:team-id ::sm/uuid]
|
||||||
[:member-id ::sm/uuid]
|
[:member-id ::sm/uuid]
|
||||||
[:role schema:role]])
|
[:role ::tt/role]])
|
||||||
|
|
||||||
(sv/defmethod ::update-team-member-role
|
(sv/defmethod ::update-team-member-role
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
@ -755,536 +751,3 @@
|
||||||
{:id team-id})
|
{:id team-id})
|
||||||
|
|
||||||
(assoc team :photo-id (:id photo)))))
|
(assoc team :photo-id (:id photo)))))
|
||||||
|
|
||||||
;; --- Mutation: Create Team Invitation
|
|
||||||
|
|
||||||
(def sql:upsert-team-invitation
|
|
||||||
"insert into team_invitation(id, team_id, email_to, role, valid_until, created_by)
|
|
||||||
values (?, ?, ?, ?, ?, ?)
|
|
||||||
on conflict(team_id, email_to) do
|
|
||||||
update set role = ?, valid_until = ?, updated_at = now()
|
|
||||||
returning *")
|
|
||||||
|
|
||||||
(defn- create-invitation-token
|
|
||||||
[cfg {:keys [profile-id valid-until team-id member-id member-email role]}]
|
|
||||||
(tokens/generate (::setup/props cfg)
|
|
||||||
{:iss :team-invitation
|
|
||||||
:exp valid-until
|
|
||||||
:profile-id profile-id
|
|
||||||
:role role
|
|
||||||
:team-id team-id
|
|
||||||
:member-email member-email
|
|
||||||
:member-id member-id}))
|
|
||||||
|
|
||||||
(defn- create-profile-identity-token
|
|
||||||
[cfg profile-id]
|
|
||||||
|
|
||||||
(dm/assert!
|
|
||||||
"expected valid uuid for profile-id"
|
|
||||||
(uuid? profile-id))
|
|
||||||
|
|
||||||
(tokens/generate (::setup/props cfg)
|
|
||||||
{:iss :profile-identity
|
|
||||||
:profile-id profile-id
|
|
||||||
:exp (dt/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 [::sm/one-of tt/valid-roles]]
|
|
||||||
[:email ::sm/email]])
|
|
||||||
|
|
||||||
(def ^:private check-create-invitation-params!
|
|
||||||
(sm/check-fn schema:create-invitation))
|
|
||||||
|
|
||||||
(defn- create-invitation
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
|
||||||
|
|
||||||
(dm/assert!
|
|
||||||
"expected valid connection on cfg parameter"
|
|
||||||
(db/connection? conn))
|
|
||||||
|
|
||||||
(dm/assert!
|
|
||||||
"expected valid params for `create-invitation` fn"
|
|
||||||
(check-create-invitation-params! params))
|
|
||||||
|
|
||||||
(let [email (profile/clean-email email)
|
|
||||||
member (profile/get-profile-by-email conn email)]
|
|
||||||
|
|
||||||
(check-profile-muted conn member)
|
|
||||||
(check-email-bounce conn email true)
|
|
||||||
(check-email-spam conn email true)
|
|
||||||
|
|
||||||
;; 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)}
|
|
||||||
(role->params role))]
|
|
||||||
|
|
||||||
;; Insert the invited member to the team
|
|
||||||
(db/insert! conn :team-profile-rel 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)
|
|
||||||
|
|
||||||
(let [id (uuid/next)
|
|
||||||
expire (dt/in-future "168h") ;; 7 days
|
|
||||||
invitation (db/exec-one! conn [sql:upsert-team-invitation id
|
|
||||||
(:id team) (str/lower email)
|
|
||||||
(name role) expire
|
|
||||||
(:id profile)
|
|
||||||
(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)
|
|
||||||
: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 (if updated?
|
|
||||||
"update-team-invitation"
|
|
||||||
"create-team-invitation")
|
|
||||||
event (-> (audit/event-from-rpc-params params)
|
|
||||||
(assoc ::audit/name evname)
|
|
||||||
(assoc ::audit/props props))]
|
|
||||||
(audit/submit! cfg event))
|
|
||||||
|
|
||||||
(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)
|
|
||||||
:token itoken
|
|
||||||
:extra-data ptoken})
|
|
||||||
|
|
||||||
itoken))))
|
|
||||||
|
|
||||||
(defn- add-user-to-team
|
|
||||||
[conn profile team role email]
|
|
||||||
|
|
||||||
(let [team-id (:id team)
|
|
||||||
member (db/get* conn :profile
|
|
||||||
{:email (str/lower email)}
|
|
||||||
{::sql/columns [:id :email]})
|
|
||||||
params (merge
|
|
||||||
{:team-id team-id
|
|
||||||
:profile-id (:id member)}
|
|
||||||
(role->params 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
|
|
||||||
(db/insert! conn :team-profile-rel 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
|
|
||||||
:invited-by (:fullname profile)
|
|
||||||
:team (:name team)
|
|
||||||
:team-id (:id team)})))
|
|
||||||
|
|
||||||
(def sql:valid-requests-email
|
|
||||||
"SELECT p.email
|
|
||||||
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()")
|
|
||||||
|
|
||||||
(defn- get-valid-requests-email
|
|
||||||
[conn team-id]
|
|
||||||
(db/exec! conn [sql:valid-requests-email team-id]))
|
|
||||||
|
|
||||||
(def ^:private xf:map-email
|
|
||||||
(map :email))
|
|
||||||
|
|
||||||
(defn- create-team-invitations
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
|
|
||||||
(let [join-requests (into #{} xf:map-email
|
|
||||||
(get-valid-requests-email conn (:id team)))
|
|
||||||
team-members (into #{} xf:map-email
|
|
||||||
(get-team-members conn (:id team)))
|
|
||||||
|
|
||||||
invitations (into #{}
|
|
||||||
(comp
|
|
||||||
;; We don't re-send inviation to
|
|
||||||
;; already existing members
|
|
||||||
(remove team-members)
|
|
||||||
;; We don't send invitations to
|
|
||||||
;; join-requested members
|
|
||||||
(remove join-requests)
|
|
||||||
(map (fn [email] (assoc params :email email)))
|
|
||||||
(keep (partial create-invitation cfg)))
|
|
||||||
emails)]
|
|
||||||
|
|
||||||
;; For requested invitations, do not send invitation emails, add
|
|
||||||
;; the user directly to the team
|
|
||||||
(->> (filter join-requests emails)
|
|
||||||
(run! (partial add-user-to-team conn profile team role)))
|
|
||||||
|
|
||||||
invitations))
|
|
||||||
|
|
||||||
(def ^:private schema:create-team-invitations
|
|
||||||
[:map {:title "create-team-invitations"}
|
|
||||||
[:team-id ::sm/uuid]
|
|
||||||
[: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."
|
|
||||||
{::doc/added "1.17"
|
|
||||||
::sm/params schema:create-team-invitations}
|
|
||||||
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
|
|
||||||
(let [perms (get-permissions cfg profile-id team-id)
|
|
||||||
profile (db/get-by-id cfg :profile profile-id)
|
|
||||||
emails (into #{} (map profile/clean-email) emails)]
|
|
||||||
|
|
||||||
(when-not (:is-admin perms)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :insufficient-permissions))
|
|
||||||
|
|
||||||
(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))
|
|
||||||
|
|
||||||
(-> cfg
|
|
||||||
(assoc ::quotes/profile-id profile-id)
|
|
||||||
(assoc ::quotes/team-id team-id)
|
|
||||||
(assoc ::quotes/incr (count emails))
|
|
||||||
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
|
|
||||||
{::quotes/id ::quotes/profiles-per-team}))
|
|
||||||
|
|
||||||
;; Check if the current profile is allowed to send emails
|
|
||||||
(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)
|
|
||||||
(assoc :emails emails)))]
|
|
||||||
|
|
||||||
(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 schema:role]])
|
|
||||||
|
|
||||||
(sv/defmethod ::create-team-with-invitations
|
|
||||||
{::doc/added "1.17"
|
|
||||||
::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 (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 ::audit/name "create-team")
|
|
||||||
(assoc ::audit/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"
|
|
||||||
::sm/params schema:get-team-invitation-token}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
|
|
||||||
(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 schema:role]])
|
|
||||||
|
|
||||||
(sv/defmethod ::update-team-invitation-role
|
|
||||||
{::doc/added "1.17"
|
|
||||||
::sm/params schema:update-team-invitation-role}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [perms (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 (dt/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}
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [perms (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 sql:upsert-team-access-request
|
|
||||||
"INSERT INTO team_access_request (id, team_id, requester_id, valid_until, auto_join_until)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
ON conflict(id)
|
|
||||||
DO UPDATE SET valid_until = ?, auto_join_until = ?, updated_at = now()
|
|
||||||
RETURNING *")
|
|
||||||
|
|
||||||
|
|
||||||
(def sql:team-access-request
|
|
||||||
"SELECT id, (valid_until < now()) AS expired
|
|
||||||
FROM team_access_request
|
|
||||||
WHERE team_id = ?
|
|
||||||
AND requester_id = ?")
|
|
||||||
|
|
||||||
(def sql:team-owner
|
|
||||||
"SELECT profile_id
|
|
||||||
FROM team_profile_rel
|
|
||||||
WHERE team_id = ?
|
|
||||||
AND is_owner = true")
|
|
||||||
|
|
||||||
|
|
||||||
(defn- create-team-access-request
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [team requester team-owner file is-viewer] :as params}]
|
|
||||||
(let [old-request (->> (db/exec-one! conn [sql:team-access-request (:id team) (:id requester)])
|
|
||||||
(decode-row))]
|
|
||||||
(when (false? (:expired old-request))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :request-already-sent
|
|
||||||
:hint "you have already made a request to join this team less than 24 hours ago"))
|
|
||||||
|
|
||||||
(let [id (or (:id old-request) (uuid/next))
|
|
||||||
valid_until (dt/in-future "24h")
|
|
||||||
auto_join_until (dt/in-future "168h") ;; 7 days
|
|
||||||
request (db/exec-one! conn [sql:upsert-team-access-request
|
|
||||||
id (:id team) (:id requester) valid_until auto_join_until
|
|
||||||
valid_until auto_join_until])
|
|
||||||
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)
|
|
||||||
page-id (when (some? file)
|
|
||||||
(-> file :data :pages first))]
|
|
||||||
|
|
||||||
;; TODO needs audit?
|
|
||||||
|
|
||||||
(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 (:id team)
|
|
||||||
:file-name (:name file)
|
|
||||||
:file-id (:id file)
|
|
||||||
:page-id page-id})
|
|
||||||
|
|
||||||
request)))
|
|
||||||
|
|
||||||
|
|
||||||
(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"
|
|
||||||
::sm/params schema:create-team-access-request}
|
|
||||||
[cfg {:keys [::rpc/profile-id file-id team-id is-viewer] :as params}]
|
|
||||||
|
|
||||||
(db/tx-run! cfg
|
|
||||||
(fn [{:keys [::db/conn] :as cfg}]
|
|
||||||
|
|
||||||
(let [requester (db/get-by-id conn :profile profile-id)
|
|
||||||
team-id (if (some? team-id)
|
|
||||||
team-id
|
|
||||||
(:id (get-team-for-file conn file-id)))
|
|
||||||
team (db/get-by-id conn :team team-id)
|
|
||||||
owner-id (->> (db/exec! conn [sql:team-owner (:id team)])
|
|
||||||
(map decode-row)
|
|
||||||
(first)
|
|
||||||
:profile-id)
|
|
||||||
team-owner (db/get-by-id conn :profile owner-id)
|
|
||||||
file (when (some? file-id)
|
|
||||||
(db/get* conn :file
|
|
||||||
{:id file-id}
|
|
||||||
{::sql/columns [:id :name :data]}))
|
|
||||||
file (when (some? file)
|
|
||||||
(assoc file :data (blob/decode (:data file))))]
|
|
||||||
|
|
||||||
;;TODO needs quotes?
|
|
||||||
|
|
||||||
(when (or (nil? requester) (nil? team) (nil? team-owner) (and (some? file-id) (nil? file)))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :invalid-parameters))
|
|
||||||
|
|
||||||
;; Check that the requester is not muted
|
|
||||||
(check-profile-muted conn requester)
|
|
||||||
|
|
||||||
;; Check that the owner is not marked as bounce nor spam
|
|
||||||
(check-email-bounce conn (:email team-owner) false)
|
|
||||||
(check-email-spam conn (:email team-owner) true)
|
|
||||||
|
|
||||||
(let [request (create-team-access-request
|
|
||||||
cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})]
|
|
||||||
(when request
|
|
||||||
(with-meta {:request request}
|
|
||||||
{::audit/props {:request 1}})))))))
|
|
||||||
|
|
573
backend/src/app/rpc/commands/teams_invitations.clj
Normal file
573
backend/src/app/rpc/commands/teams_invitations.clj
Normal file
|
@ -0,0 +1,573 @@
|
||||||
|
;; 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.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.types.team :as types.team]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.db.sql :as sql]
|
||||||
|
[app.email :as eml]
|
||||||
|
[app.loggers.audit :as audit]
|
||||||
|
[app.main :as-alias main]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.commands.files :as files]
|
||||||
|
[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]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
;; --- Mutation: Create Team Invitation
|
||||||
|
|
||||||
|
|
||||||
|
(def sql:upsert-team-invitation
|
||||||
|
"insert into team_invitation(id, team_id, email_to, created_by, role, valid_until)
|
||||||
|
values (?, ?, ?, ?, ?, ?)
|
||||||
|
on conflict(team_id, email_to) do
|
||||||
|
update set role = ?, valid_until = ?, updated_at = now()
|
||||||
|
returning *")
|
||||||
|
|
||||||
|
(defn- create-invitation-token
|
||||||
|
[cfg {:keys [profile-id valid-until team-id member-id member-email role]}]
|
||||||
|
(tokens/generate (::setup/props cfg)
|
||||||
|
{:iss :team-invitation
|
||||||
|
:exp valid-until
|
||||||
|
:profile-id profile-id
|
||||||
|
:role role
|
||||||
|
:team-id team-id
|
||||||
|
:member-email member-email
|
||||||
|
:member-id member-id}))
|
||||||
|
|
||||||
|
(defn- create-profile-identity-token
|
||||||
|
[cfg profile-id]
|
||||||
|
|
||||||
|
(dm/assert!
|
||||||
|
"expected valid uuid for profile-id"
|
||||||
|
(uuid? profile-id))
|
||||||
|
|
||||||
|
(tokens/generate (::setup/props cfg)
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id profile-id
|
||||||
|
:exp (dt/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/role]
|
||||||
|
[:email ::sm/email]])
|
||||||
|
|
||||||
|
(def ^:private check-create-invitation-params!
|
||||||
|
(sm/check-fn schema:create-invitation))
|
||||||
|
|
||||||
|
(defn- create-invitation
|
||||||
|
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
||||||
|
|
||||||
|
(dm/assert!
|
||||||
|
"expected valid connection on cfg parameter"
|
||||||
|
(db/connection? conn))
|
||||||
|
|
||||||
|
(dm/assert!
|
||||||
|
"expected valid params for `create-invitation` fn"
|
||||||
|
(check-create-invitation-params! params))
|
||||||
|
|
||||||
|
(let [email (profile/clean-email email)
|
||||||
|
member (profile/get-profile-by-email conn email)]
|
||||||
|
|
||||||
|
(teams/check-profile-muted conn member)
|
||||||
|
(teams/check-email-bounce conn email true)
|
||||||
|
(teams/check-email-spam conn email true)
|
||||||
|
|
||||||
|
;; 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))]
|
||||||
|
|
||||||
|
;; Insert the invited member to the team
|
||||||
|
(db/insert! conn :team-profile-rel 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)
|
||||||
|
|
||||||
|
(let [id (uuid/next)
|
||||||
|
expire (dt/in-future "168h") ;; 7 days
|
||||||
|
invitation (db/exec-one! conn [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)
|
||||||
|
: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 (if updated?
|
||||||
|
"update-team-invitation"
|
||||||
|
"create-team-invitation")
|
||||||
|
event (-> (audit/event-from-rpc-params params)
|
||||||
|
(assoc ::audit/name evname)
|
||||||
|
(assoc ::audit/props props))]
|
||||||
|
(audit/submit! cfg event))
|
||||||
|
|
||||||
|
(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)
|
||||||
|
:token itoken
|
||||||
|
:extra-data ptoken})
|
||||||
|
|
||||||
|
itoken))))
|
||||||
|
|
||||||
|
(defn- add-user-to-team
|
||||||
|
[conn profile team role email]
|
||||||
|
|
||||||
|
(let [team-id (:id team)
|
||||||
|
member (db/get* conn :profile
|
||||||
|
{:email (str/lower email)}
|
||||||
|
{::sql/columns [:id :email]})
|
||||||
|
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
|
||||||
|
(db/insert! conn :team-profile-rel 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
|
||||||
|
:invited-by (:fullname profile)
|
||||||
|
:team (:name team)
|
||||||
|
:team-id (:id team)})))
|
||||||
|
|
||||||
|
(def sql:valid-requests-email
|
||||||
|
"SELECT p.email
|
||||||
|
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()")
|
||||||
|
|
||||||
|
(defn- get-valid-requests-email
|
||||||
|
[conn team-id]
|
||||||
|
(db/exec! conn [sql:valid-requests-email team-id]))
|
||||||
|
|
||||||
|
(def ^:private xf:map-email
|
||||||
|
(map :email))
|
||||||
|
|
||||||
|
(defn- create-team-invitations
|
||||||
|
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails] :as params}]
|
||||||
|
(let [join-requests (into #{} xf:map-email
|
||||||
|
(get-valid-requests-email conn (:id team)))
|
||||||
|
team-members (into #{} xf:map-email
|
||||||
|
(teams/get-team-members conn (:id team)))
|
||||||
|
|
||||||
|
invitations (into #{}
|
||||||
|
(comp
|
||||||
|
;; We don't re-send inviation to
|
||||||
|
;; already existing members
|
||||||
|
(remove team-members)
|
||||||
|
;; We don't send invitations to
|
||||||
|
;; join-requested members
|
||||||
|
(remove join-requests)
|
||||||
|
(map (fn [email] (assoc params :email email)))
|
||||||
|
(keep (partial create-invitation cfg)))
|
||||||
|
emails)]
|
||||||
|
|
||||||
|
;; For requested invitations, do not send invitation emails, add
|
||||||
|
;; the user directly to the team
|
||||||
|
(->> (filter join-requests emails)
|
||||||
|
(run! (partial add-user-to-team conn profile team role)))
|
||||||
|
|
||||||
|
invitations))
|
||||||
|
|
||||||
|
(def ^:private schema:create-team-invitations
|
||||||
|
[:map {:title "create-team-invitations"}
|
||||||
|
[:team-id ::sm/uuid]
|
||||||
|
[:role ::types.team/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."
|
||||||
|
{::doc/added "1.17"
|
||||||
|
::doc/module :teams
|
||||||
|
::sm/params schema:create-team-invitations}
|
||||||
|
[cfg {:keys [::rpc/profile-id team-id emails] :as params}]
|
||||||
|
(let [perms (teams/get-permissions cfg profile-id team-id)
|
||||||
|
profile (db/get-by-id cfg :profile profile-id)
|
||||||
|
emails (into #{} (map profile/clean-email) emails)]
|
||||||
|
|
||||||
|
(when-not (:is-admin perms)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :insufficient-permissions))
|
||||||
|
|
||||||
|
(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))
|
||||||
|
|
||||||
|
(-> cfg
|
||||||
|
(assoc ::quotes/profile-id profile-id)
|
||||||
|
(assoc ::quotes/team-id team-id)
|
||||||
|
(assoc ::quotes/incr (count emails))
|
||||||
|
(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)
|
||||||
|
(assoc :emails emails)))]
|
||||||
|
|
||||||
|
(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/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 ::audit/name "create-team")
|
||||||
|
(assoc ::audit/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/role]])
|
||||||
|
|
||||||
|
(sv/defmethod ::update-team-invitation-role
|
||||||
|
{::doc/added "1.17"
|
||||||
|
::doc/module :teams
|
||||||
|
::sm/params schema:update-team-invitation-role}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(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 (dt/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}
|
||||||
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(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"
|
||||||
|
[conn team-id profile-id]
|
||||||
|
(when-let [request (db/get* conn :team-access-request
|
||||||
|
{:team-id team-id
|
||||||
|
:requester-id profile-id})]
|
||||||
|
(when (dt/is-after? (:valid-until request) (dt/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"
|
||||||
|
[conn team-id requester-id]
|
||||||
|
(check-existing-team-access-request conn team-id requester-id)
|
||||||
|
(let [valid-until (dt/in-future {:hours 24})
|
||||||
|
auto-join-until (dt/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 (files/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))
|
||||||
|
|
||||||
|
request (upsert-team-access-request conn team-id profile-id)]
|
||||||
|
|
||||||
|
;; FIXME missing quotes
|
||||||
|
|
||||||
|
(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 [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}})))
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
|
[app.common.types.team :as types.team]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.sql :as-alias sql]
|
[app.db.sql :as-alias sql]
|
||||||
|
@ -16,7 +17,6 @@
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.commands.teams :as teams]
|
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.quotes :as quotes]
|
[app.rpc.quotes :as quotes]
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
params (merge
|
params (merge
|
||||||
{:team-id team-id
|
{:team-id team-id
|
||||||
:profile-id (:id member)}
|
:profile-id (:id member)}
|
||||||
(teams/role->params role))]
|
(get types.team/permissions-for-role role))]
|
||||||
|
|
||||||
;; Do not allow blocked users accept invitations.
|
;; Do not allow blocked users accept invitations.
|
||||||
(when (:is-blocked member)
|
(when (:is-blocked member)
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
[:iss :keyword]
|
[:iss :keyword]
|
||||||
[:exp ::dt/instant]
|
[:exp ::dt/instant]
|
||||||
[:profile-id ::sm/uuid]
|
[:profile-id ::sm/uuid]
|
||||||
[:role teams/schema:role]
|
[:role ::types.team/role]
|
||||||
[:team-id ::sm/uuid]
|
[:team-id ::sm/uuid]
|
||||||
[:member-email ::sm/email]
|
[:member-email ::sm/email]
|
||||||
[:member-id {:optional true} ::sm/uuid]])
|
[:member-id {:optional true} ::sm/uuid]])
|
||||||
|
|
|
@ -158,6 +158,7 @@
|
||||||
:iso8601 (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s)))))
|
:iso8601 (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s)))))
|
||||||
|
|
||||||
(defn is-after?
|
(defn is-after?
|
||||||
|
"Analgous to: da > db"
|
||||||
[da db]
|
[da db]
|
||||||
(.isAfter ^Instant da ^Instant db))
|
(.isAfter ^Instant da ^Instant db))
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
(def default
|
(def default
|
||||||
"A common flags that affects both: backend and frontend."
|
"A common flags that affects both: backend and frontend."
|
||||||
[:enable-registration
|
[:enable-registration
|
||||||
|
:enable-export-file-v3
|
||||||
:enable-login-with-password])
|
:enable-login-with-password])
|
||||||
|
|
||||||
(defn parse
|
(defn parse
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns app.common.types.team)
|
(ns app.common.types.team
|
||||||
|
(:require
|
||||||
|
[app.common.schema :as sm]))
|
||||||
|
|
||||||
(def valid-roles
|
(def valid-roles
|
||||||
#{:owner :admin :editor :viewer})
|
#{:owner :admin :editor :viewer})
|
||||||
|
@ -15,3 +17,4 @@
|
||||||
:admin {:can-edit true :is-admin true :is-owner false}
|
:admin {:can-edit true :is-admin true :is-owner false}
|
||||||
:owner {:can-edit true :is-admin true :is-owner true}})
|
:owner {:can-edit true :is-admin true :is-owner true}})
|
||||||
|
|
||||||
|
(sm/register! ::role [::sm/one-of valid-roles])
|
||||||
|
|
|
@ -55,13 +55,14 @@
|
||||||
(let [prev-team-id (:current-team-id state)]
|
(let [prev-team-id (:current-team-id state)]
|
||||||
(cond-> state
|
(cond-> state
|
||||||
(not= prev-team-id id)
|
(not= prev-team-id id)
|
||||||
(-> (dissoc :current-team-id)
|
(-> (dissoc :current-team-initialized)
|
||||||
(dissoc :dashboard-files)
|
(dissoc :dashboard-files)
|
||||||
(dissoc :dashboard-projects)
|
(dissoc :dashboard-projects)
|
||||||
(dissoc :dashboard-shared-files)
|
(dissoc :dashboard-shared-files)
|
||||||
(dissoc :dashboard-recent-files)
|
(dissoc :dashboard-recent-files)
|
||||||
(dissoc :dashboard-team-members)
|
(dissoc :dashboard-team-members)
|
||||||
(dissoc :dashboard-team-stats)
|
(dissoc :dashboard-team-stats)
|
||||||
|
(assoc :current-team-id id)
|
||||||
(update :workspace-global dissoc :default-font)))))
|
(update :workspace-global dissoc :default-font)))))
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
|
@ -73,9 +74,9 @@
|
||||||
;; fetch teams must be first in case the team doesn't exist
|
;; fetch teams must be first in case the team doesn't exist
|
||||||
(ptk/watch (du/fetch-teams) state stream)
|
(ptk/watch (du/fetch-teams) state stream)
|
||||||
(ptk/watch (df/load-team-fonts id) state stream)
|
(ptk/watch (df/load-team-fonts id) state stream)
|
||||||
(ptk/watch (fetch-projects id) state stream)
|
(ptk/watch (fetch-projects) state stream)
|
||||||
(ptk/watch (fetch-team-members id) state stream)
|
(ptk/watch (fetch-team-members) state stream)
|
||||||
(ptk/watch (du/fetch-users {:team-id id}) state stream)
|
(ptk/watch (du/fetch-users) state stream)
|
||||||
|
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::dws/message))
|
(rx/filter (ptk/type? ::dws/message))
|
||||||
|
@ -92,7 +93,9 @@
|
||||||
(rx/observe-on :async)
|
(rx/observe-on :async)
|
||||||
(rx/mapcat deref)
|
(rx/mapcat deref)
|
||||||
(rx/filter #(= id (:id %)))
|
(rx/filter #(= id (:id %)))
|
||||||
(rx/map du/set-current-team)))
|
(rx/mapcat (fn [team]
|
||||||
|
(rx/of (du/set-current-team team)
|
||||||
|
#(assoc % :current-team-initialized true))))))
|
||||||
|
|
||||||
(rx/take-until stopper))))))
|
(rx/take-until stopper))))))
|
||||||
|
|
||||||
|
@ -114,12 +117,15 @@
|
||||||
(assoc state :dashboard-team-members (d/index-by :id members)))))
|
(assoc state :dashboard-team-members (d/index-by :id members)))))
|
||||||
|
|
||||||
(defn fetch-team-members
|
(defn fetch-team-members
|
||||||
[team-id]
|
([] (fetch-team-members nil))
|
||||||
(ptk/reify ::fetch-team-members
|
([team-id]
|
||||||
ptk/WatchEvent
|
(ptk/reify ::fetch-team-members
|
||||||
(watch [_ _ _]
|
ptk/WatchEvent
|
||||||
(->> (rp/cmd! :get-team-members {:team-id team-id})
|
(watch [_ state _]
|
||||||
(rx/map team-members-fetched)))))
|
(let [team-id (or team-id (:current-team-id state))]
|
||||||
|
(assert (uuid? team-id) "expected team-id to be resolved")
|
||||||
|
(->> (rp/cmd! :get-team-members {:team-id team-id})
|
||||||
|
(rx/map team-members-fetched)))))))
|
||||||
|
|
||||||
;; --- EVENT: fetch-team-stats
|
;; --- EVENT: fetch-team-stats
|
||||||
|
|
||||||
|
@ -185,12 +191,13 @@
|
||||||
(assoc state :dashboard-projects projects)))))
|
(assoc state :dashboard-projects projects)))))
|
||||||
|
|
||||||
(defn fetch-projects
|
(defn fetch-projects
|
||||||
[team-id]
|
[]
|
||||||
(ptk/reify ::fetch-projects
|
(ptk/reify ::fetch-projects
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ state _]
|
||||||
(->> (rp/cmd! :get-projects {:team-id team-id})
|
(let [team-id (:current-team-id state)]
|
||||||
(rx/map projects-fetched)))))
|
(->> (rp/cmd! :get-projects {:team-id team-id})
|
||||||
|
(rx/map projects-fetched))))))
|
||||||
|
|
||||||
;; --- EVENT: search
|
;; --- EVENT: search
|
||||||
|
|
||||||
|
@ -284,15 +291,13 @@
|
||||||
(update :dashboard-files d/merge files))))))
|
(update :dashboard-files d/merge files))))))
|
||||||
|
|
||||||
(defn fetch-recent-files
|
(defn fetch-recent-files
|
||||||
([] (fetch-recent-files nil))
|
[]
|
||||||
([team-id]
|
(ptk/reify ::fetch-recent-files
|
||||||
(ptk/reify ::fetch-recent-files
|
ptk/WatchEvent
|
||||||
ptk/WatchEvent
|
(watch [_ state _]
|
||||||
(watch [_ state _]
|
(let [team-id (:current-team-id state)]
|
||||||
(let [team-id (or team-id (:current-team-id state))]
|
(->> (rp/cmd! :get-team-recent-files {:team-id team-id})
|
||||||
(->> (rp/cmd! :get-team-recent-files {:team-id team-id})
|
(rx/map recent-files-fetched))))))
|
||||||
(rx/map recent-files-fetched)))))))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- EVENT: fetch-template-files
|
;; --- EVENT: fetch-template-files
|
||||||
|
|
||||||
|
@ -491,7 +496,7 @@
|
||||||
params (assoc params :team-id team-id)]
|
params (assoc params :team-id team-id)]
|
||||||
(->> (rp/cmd! :update-team-member-role params)
|
(->> (rp/cmd! :update-team-member-role params)
|
||||||
(rx/mapcat (fn [_]
|
(rx/mapcat (fn [_]
|
||||||
(rx/of (fetch-team-members team-id)
|
(rx/of (fetch-team-members)
|
||||||
(du/fetch-teams)
|
(du/fetch-teams)
|
||||||
(ptk/data-event ::ev/event
|
(ptk/data-event ::ev/event
|
||||||
{::ev/name "update-team-member-role"
|
{::ev/name "update-team-member-role"
|
||||||
|
@ -509,7 +514,7 @@
|
||||||
params (assoc params :team-id team-id)]
|
params (assoc params :team-id team-id)]
|
||||||
(->> (rp/cmd! :delete-team-member params)
|
(->> (rp/cmd! :delete-team-member params)
|
||||||
(rx/mapcat (fn [_]
|
(rx/mapcat (fn [_]
|
||||||
(rx/of (fetch-team-members team-id)
|
(rx/of (fetch-team-members)
|
||||||
(du/fetch-teams)
|
(du/fetch-teams)
|
||||||
(ptk/data-event ::ev/event
|
(ptk/data-event ::ev/event
|
||||||
{::ev/name "delete-team-member"
|
{::ev/name "delete-team-member"
|
||||||
|
|
|
@ -561,17 +561,17 @@
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
(defn fetch-users
|
(defn fetch-users
|
||||||
[{:keys [team-id]}]
|
[]
|
||||||
(dm/assert! (uuid? team-id))
|
|
||||||
(letfn [(fetched [users state]
|
(letfn [(fetched [users state]
|
||||||
(->> users
|
(->> users
|
||||||
(d/index-by :id)
|
(d/index-by :id)
|
||||||
(assoc state :users)))]
|
(assoc state :users)))]
|
||||||
(ptk/reify ::fetch-team-users
|
(ptk/reify ::fetch-team-users
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ state _]
|
||||||
(->> (rp/cmd! :get-team-users {:team-id team-id})
|
(let [team-id (:current-team-id state)]
|
||||||
(rx/map #(partial fetched %)))))))
|
(->> (rp/cmd! :get-team-users {:team-id team-id})
|
||||||
|
(rx/map #(partial fetched %))))))))
|
||||||
|
|
||||||
(defn fetch-file-comments-users
|
(defn fetch-file-comments-users
|
||||||
[{:keys [team-id]}]
|
[{:keys [team-id]}]
|
||||||
|
|
|
@ -420,14 +420,25 @@
|
||||||
(into [] (distinct) (conj coll item)))
|
(into [] (distinct) (conj coll item)))
|
||||||
|
|
||||||
(mf/defc multi-input
|
(mf/defc multi-input
|
||||||
[{:keys [form label class name trim valid-item-fn caution-item-fn on-submit invite-email] :as props}]
|
[{:keys [form label class name trim valid-item-fn caution-item-fn on-submit] :as props}]
|
||||||
(let [form (or form (mf/use-ctx form-ctx))
|
(let [form (or form (mf/use-ctx form-ctx))
|
||||||
input-name (get props :name)
|
input-name (get props :name)
|
||||||
touched? (get-in @form [:touched input-name])
|
touched? (get-in @form [:touched input-name])
|
||||||
error (get-in @form [:errors input-name])
|
error (get-in @form [:errors input-name])
|
||||||
focus? (mf/use-state false)
|
focus? (mf/use-state false)
|
||||||
|
|
||||||
items (mf/use-state [])
|
items (mf/use-state
|
||||||
|
(fn []
|
||||||
|
(let [initial (get-in @form [:data input-name])]
|
||||||
|
(if (or (vector? initial)
|
||||||
|
(set? initial))
|
||||||
|
(mapv (fn [val]
|
||||||
|
{:text val
|
||||||
|
:valid (valid-item-fn val)
|
||||||
|
:caution (caution-item-fn val)})
|
||||||
|
initial)
|
||||||
|
[]))))
|
||||||
|
|
||||||
value (mf/use-state "")
|
value (mf/use-state "")
|
||||||
result (hooks/use-equal-memo @items)
|
result (hooks/use-equal-memo @items)
|
||||||
|
|
||||||
|
@ -527,13 +538,8 @@
|
||||||
(let [val (cond-> @value trim str/trim)
|
(let [val (cond-> @value trim str/trim)
|
||||||
values (conj-dedup result {:text val :valid (valid-item-fn val)})
|
values (conj-dedup result {:text val :valid (valid-item-fn val)})
|
||||||
values (filterv #(:valid %) values)]
|
values (filterv #(:valid %) values)]
|
||||||
(update-form! values)))
|
|
||||||
|
|
||||||
(mf/with-effect []
|
(update-form! values)))
|
||||||
(when invite-email
|
|
||||||
(swap! items conj-dedup {:text (str/trim invite-email)
|
|
||||||
:valid (valid-item-fn invite-email)
|
|
||||||
:caution (caution-item-fn invite-email)})))
|
|
||||||
|
|
||||||
[:div {:class klass}
|
[:div {:class klass}
|
||||||
[:input {:id (name input-name)
|
[:input {:id (name input-name)
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
(assoc :project-id (uuid project-id)))))
|
(assoc :project-id (uuid project-id)))))
|
||||||
|
|
||||||
(mf/defc dashboard-content
|
(mf/defc dashboard-content
|
||||||
[{:keys [team projects project section search-term profile invite-email] :as props}]
|
[{:keys [team projects project section search-term profile] :as props}]
|
||||||
(let [container (mf/use-ref)
|
(let [container (mf/use-ref)
|
||||||
content-width (mf/use-state 0)
|
content-width (mf/use-state 0)
|
||||||
project-id (:id project)
|
project-id (:id project)
|
||||||
|
@ -143,7 +143,7 @@
|
||||||
[:& libraries-page {:team team}]
|
[:& libraries-page {:team team}]
|
||||||
|
|
||||||
:dashboard-team-members
|
:dashboard-team-members
|
||||||
[:& team-members-page {:team team :profile profile :invite-email invite-email}]
|
[:& team-members-page {:team team :profile profile}]
|
||||||
|
|
||||||
:dashboard-team-invitations
|
:dashboard-team-invitations
|
||||||
[:& team-invitations-page {:team team}]
|
[:& team-invitations-page {:team team}]
|
||||||
|
@ -156,8 +156,8 @@
|
||||||
|
|
||||||
nil)]))
|
nil)]))
|
||||||
|
|
||||||
(def dashboard-initialized
|
(def ref:dashboard-initialized
|
||||||
(l/derived :current-team-id st/state))
|
(l/derived :current-team-initialized st/state))
|
||||||
|
|
||||||
(defn use-plugin-register
|
(defn use-plugin-register
|
||||||
[plugin-url team-id project-id]
|
[plugin-url team-id project-id]
|
||||||
|
@ -231,16 +231,13 @@
|
||||||
|
|
||||||
plugin-url (-> route :query-params :plugin)
|
plugin-url (-> route :query-params :plugin)
|
||||||
|
|
||||||
invite-email (-> route :query-params :invite-email)
|
|
||||||
|
|
||||||
team (mf/deref refs/team)
|
team (mf/deref refs/team)
|
||||||
|
|
||||||
projects (mf/deref refs/dashboard-projects)
|
projects (mf/deref refs/dashboard-projects)
|
||||||
project (get projects project-id)
|
project (get projects project-id)
|
||||||
|
|
||||||
default-project (->> projects vals (d/seek :is-default))
|
default-project (->> projects vals (d/seek :is-default))
|
||||||
|
|
||||||
initialized? (mf/deref dashboard-initialized)]
|
initialized? (mf/deref ref:dashboard-initialized)]
|
||||||
|
|
||||||
(hooks/use-shortcuts ::dashboard sc/shortcuts)
|
(hooks/use-shortcuts ::dashboard sc/shortcuts)
|
||||||
|
|
||||||
|
@ -287,5 +284,4 @@
|
||||||
:project project
|
:project project
|
||||||
:section section
|
:section section
|
||||||
:search-term search-term
|
:search-term search-term
|
||||||
:team team
|
:team team}])])]]]))
|
||||||
:invite-email invite-email}])])]]]))
|
|
||||||
|
|
|
@ -135,7 +135,7 @@
|
||||||
(st/emit! (ntf/success (tr "dashboard.success-move-file"))))
|
(st/emit! (ntf/success (tr "dashboard.success-move-file"))))
|
||||||
(if (or navigate (not= team-id current-team-id))
|
(if (or navigate (not= team-id current-team-id))
|
||||||
(st/emit! (dd/go-to-files team-id project-id))
|
(st/emit! (dd/go-to-files team-id project-id))
|
||||||
(st/emit! (dd/fetch-recent-files team-id)
|
(st/emit! (dd/fetch-recent-files)
|
||||||
(dd/clear-selected-files))))
|
(dd/clear-selected-files))))
|
||||||
|
|
||||||
on-move-accept
|
on-move-accept
|
||||||
|
@ -255,18 +255,20 @@
|
||||||
:id "file-move-multi"
|
:id "file-move-multi"
|
||||||
:options sub-options})
|
:options sub-options})
|
||||||
|
|
||||||
{:name (tr "dashboard.export-binary-multi" file-count)
|
(when-not (contains? cf/flags :export-file-v3)
|
||||||
:id "file-binary-export-multi"
|
{:name (tr "dashboard.export-binary-multi" file-count)
|
||||||
:handler on-export-binary-files}
|
:id "file-binary-export-multi"
|
||||||
|
:handler on-export-binary-files})
|
||||||
|
|
||||||
(when (contains? cf/flags :export-file-v3)
|
(when (contains? cf/flags :export-file-v3)
|
||||||
{:name (tr "dashboard.export-binary-multi-v3" file-count)
|
{:name (tr "dashboard.export-binary-multi-v3" file-count)
|
||||||
:id "file-binary-export-multi-v3"
|
:id "file-binary-export-multi-v3"
|
||||||
:handler on-export-binary-files-v3})
|
:handler on-export-binary-files-v3})
|
||||||
|
|
||||||
{:name (tr "dashboard.export-standard-multi" file-count)
|
(when-not (contains? cf/flags :export-file-v3)
|
||||||
:id "file-standard-export-multi"
|
{:name (tr "dashboard.export-standard-multi" file-count)
|
||||||
:handler on-export-standard-files}
|
:id "file-standard-export-multi"
|
||||||
|
:handler on-export-standard-files})
|
||||||
|
|
||||||
(when (and (:is-shared file) can-edit)
|
(when (and (:is-shared file) can-edit)
|
||||||
{:name (tr "labels.unpublish-multi-files" file-count)
|
{:name (tr "labels.unpublish-multi-files" file-count)
|
||||||
|
@ -312,18 +314,20 @@
|
||||||
|
|
||||||
{:name :separator}
|
{:name :separator}
|
||||||
|
|
||||||
{:name (tr "dashboard.download-binary-file")
|
(when-not (contains? cf/flags :export-file-v3)
|
||||||
:id "download-binary-file"
|
{:name (tr "dashboard.download-binary-file")
|
||||||
:handler on-export-binary-files}
|
:id "download-binary-file"
|
||||||
|
:handler on-export-binary-files})
|
||||||
|
|
||||||
(when (contains? cf/flags :export-file-v3)
|
(when (contains? cf/flags :export-file-v3)
|
||||||
{:name (tr "dashboard.download-binary-file-v3")
|
{:name (tr "dashboard.download-binary-file-v3")
|
||||||
:id "download-binary-file-v3"
|
:id "download-binary-file-v3"
|
||||||
:handler on-export-binary-files-v3})
|
:handler on-export-binary-files-v3})
|
||||||
|
|
||||||
{:name (tr "dashboard.download-standard-file")
|
(when-not (contains? cf/flags :export-file-v3)
|
||||||
:id "download-standard-file"
|
{:name (tr "dashboard.download-standard-file")
|
||||||
:handler on-export-standard-files}
|
:id "download-standard-file"
|
||||||
|
:handler on-export-standard-files})
|
||||||
|
|
||||||
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
|
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
|
||||||
{:name :separator})
|
{:name :separator})
|
||||||
|
|
|
@ -529,9 +529,8 @@
|
||||||
|
|
||||||
on-finish-import
|
on-finish-import
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps team-id)
|
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit! (dd/fetch-recent-files (:id team))
|
(st/emit! (dd/fetch-recent-files)
|
||||||
(dd/clear-selected-files))))
|
(dd/clear-selected-files))))
|
||||||
|
|
||||||
import-files (use-import-file project-id on-finish-import)
|
import-files (use-import-file project-id on-finish-import)
|
||||||
|
@ -571,10 +570,11 @@
|
||||||
(reset! dragging? false))))
|
(reset! dragging? false))))
|
||||||
|
|
||||||
on-drop-success
|
on-drop-success
|
||||||
(fn []
|
(mf/use-fn
|
||||||
(st/emit! (ntf/success (tr "dashboard.success-move-file"))
|
(fn []
|
||||||
(dd/fetch-recent-files (:id team))
|
(st/emit! (ntf/success (tr "dashboard.success-move-file"))
|
||||||
(dd/clear-selected-files)))
|
(dd/fetch-recent-files)
|
||||||
|
(dd/clear-selected-files))))
|
||||||
|
|
||||||
on-drop
|
on-drop
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
|
|
@ -192,8 +192,8 @@
|
||||||
(mf/deps project-id (:id team))
|
(mf/deps project-id (:id team))
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit! (dd/fetch-files {:project-id project-id})
|
(st/emit! (dd/fetch-files {:project-id project-id})
|
||||||
(dd/fetch-recent-files (:id team))
|
(dd/fetch-recent-files)
|
||||||
(dd/fetch-projects (:id team))
|
(dd/fetch-projects)
|
||||||
(dd/clear-selected-files))))
|
(dd/clear-selected-files))))
|
||||||
|
|
||||||
handle-create-click
|
handle-create-click
|
||||||
|
@ -303,17 +303,17 @@
|
||||||
[:span {:class (stl/css :placeholder-label)} (tr "dashboard.show-all-files")]
|
[:span {:class (stl/css :placeholder-label)} (tr "dashboard.show-all-files")]
|
||||||
show-more-icon])]))
|
show-more-icon])]))
|
||||||
|
|
||||||
|
(def ref:recent-files
|
||||||
(def recent-files-ref
|
|
||||||
(l/derived :dashboard-recent-files st/state))
|
(l/derived :dashboard-recent-files st/state))
|
||||||
|
|
||||||
(mf/defc projects-section
|
(mf/defc projects-section
|
||||||
[{:keys [team projects profile] :as props}]
|
{::mf/props :obj}
|
||||||
|
[{:keys [team projects profile]}]
|
||||||
|
|
||||||
(let [projects (->> (vals projects)
|
(let [projects (->> (vals projects)
|
||||||
(sort-by :modified-at)
|
(sort-by :modified-at)
|
||||||
(reverse))
|
(reverse))
|
||||||
recent-map (mf/deref recent-files-ref)
|
recent-map (mf/deref ref:recent-files)
|
||||||
permisions (:permissions team)
|
permisions (:permissions team)
|
||||||
|
|
||||||
can-edit (:can-edit permisions)
|
can-edit (:can-edit permisions)
|
||||||
|
@ -326,8 +326,6 @@
|
||||||
is-my-penpot (= (:default-team-id profile) (:id team))
|
is-my-penpot (= (:default-team-id profile) (:id team))
|
||||||
is-defalt-team? (:is-default team)
|
is-defalt-team? (:is-default team)
|
||||||
|
|
||||||
team-id (:id team)
|
|
||||||
|
|
||||||
on-close
|
on-close
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn []
|
(fn []
|
||||||
|
@ -344,8 +342,8 @@
|
||||||
(:name team))]
|
(:name team))]
|
||||||
(dom/set-html-title (tr "title.dashboard.projects" tname))))
|
(dom/set-html-title (tr "title.dashboard.projects" tname))))
|
||||||
|
|
||||||
(mf/with-effect [team-id]
|
(mf/with-effect []
|
||||||
(st/emit! (dd/fetch-recent-files team-id)
|
(st/emit! (dd/fetch-recent-files)
|
||||||
(dd/clear-selected-files)))
|
(dd/clear-selected-files)))
|
||||||
|
|
||||||
(when (seq projects)
|
(when (seq projects)
|
||||||
|
|
|
@ -59,13 +59,16 @@
|
||||||
|
|
||||||
(mf/defc header
|
(mf/defc header
|
||||||
{::mf/wrap [mf/memo]
|
{::mf/wrap [mf/memo]
|
||||||
::mf/wrap-props false}
|
::mf/props :obj}
|
||||||
[{:keys [section team invite-email]}]
|
[{:keys [section team]}]
|
||||||
(let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
|
(let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
|
||||||
on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
|
on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
|
||||||
on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
|
on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
|
||||||
on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
|
on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks)))
|
||||||
|
|
||||||
|
route (mf/deref refs/route)
|
||||||
|
invite-email (-> route :query-params :invite-email)
|
||||||
|
|
||||||
members-section? (= section :dashboard-team-members)
|
members-section? (= section :dashboard-team-members)
|
||||||
settings-section? (= section :dashboard-team-settings)
|
settings-section? (= section :dashboard-team-settings)
|
||||||
invitations-section? (= section :dashboard-team-invitations)
|
invitations-section? (= section :dashboard-team-invitations)
|
||||||
|
@ -74,14 +77,14 @@
|
||||||
|
|
||||||
on-invite-member
|
on-invite-member
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps team)
|
(mf/deps team invite-email)
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit! (modal/show {:type :invite-members
|
(st/emit! (modal/show {:type :invite-members
|
||||||
:team team
|
:team team
|
||||||
:origin :team
|
:origin :team
|
||||||
:invite-email invite-email}))))]
|
:invite-email invite-email}))))]
|
||||||
|
|
||||||
(mf/with-effect []
|
(mf/with-effect [team invite-email]
|
||||||
(when invite-email
|
(when invite-email
|
||||||
(on-invite-member)))
|
(on-invite-member)))
|
||||||
|
|
||||||
|
@ -134,7 +137,7 @@
|
||||||
(mf/defc invite-members-modal
|
(mf/defc invite-members-modal
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
::mf/register-as :invite-members
|
::mf/register-as :invite-members
|
||||||
::mf/wrap-props false}
|
::mf/props :obj}
|
||||||
[{:keys [team origin invite-email]}]
|
[{:keys [team origin invite-email]}]
|
||||||
(let [members-map (mf/deref refs/dashboard-team-members)
|
(let [members-map (mf/deref refs/dashboard-team-members)
|
||||||
perms (:permissions team)
|
perms (:permissions team)
|
||||||
|
@ -143,8 +146,10 @@
|
||||||
(get-available-roles perms))
|
(get-available-roles perms))
|
||||||
team-id (:id team)
|
team-id (:id team)
|
||||||
|
|
||||||
initial (mf/with-memo [team-id]
|
initial (mf/with-memo [team-id invite-email]
|
||||||
{:role "editor" :team-id team-id})
|
(if invite-email
|
||||||
|
{:role "editor" :team-id team-id :emails #{invite-email}}
|
||||||
|
{:role "editor" :team-id team-id}))
|
||||||
|
|
||||||
form (fm/use-form :schema schema:invite-member-form
|
form (fm/use-form :schema schema:invite-member-form
|
||||||
:initial initial)
|
:initial initial)
|
||||||
|
@ -159,6 +164,7 @@
|
||||||
(st/emit! (ntf/success (tr "notifications.invitation-email-sent"))))
|
(st/emit! (ntf/success (tr "notifications.invitation-email-sent"))))
|
||||||
|
|
||||||
(st/emit! (modal/hide)
|
(st/emit! (modal/hide)
|
||||||
|
(dd/fetch-team-members)
|
||||||
(dd/fetch-team-invitations)))
|
(dd/fetch-team-invitations)))
|
||||||
|
|
||||||
on-error
|
on-error
|
||||||
|
@ -230,8 +236,7 @@
|
||||||
:trim true
|
:trim true
|
||||||
:valid-item-fn sm/parse-email
|
:valid-item-fn sm/parse-email
|
||||||
:caution-item-fn current-members-emails
|
:caution-item-fn current-members-emails
|
||||||
:label (tr "modals.invite-member.emails")
|
:label (tr "modals.invite-member.emails")}]]
|
||||||
:invite-email invite-email}]]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :action-buttons)}
|
[:div {:class (stl/css :action-buttons)}
|
||||||
[:> fm/submit-button*
|
[:> fm/submit-button*
|
||||||
|
@ -245,7 +250,7 @@
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(mf/defc member-info
|
(mf/defc member-info
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [member profile]}]
|
[{:keys [member profile]}]
|
||||||
(let [is-you? (= (:id profile) (:id member))]
|
(let [is-you? (= (:id profile) (:id member))]
|
||||||
[:*
|
[:*
|
||||||
|
@ -258,7 +263,7 @@
|
||||||
[:div {:class (stl/css :member-email)} (:email member)]]]))
|
[:div {:class (stl/css :member-email)} (:email member)]]]))
|
||||||
|
|
||||||
(mf/defc rol-info
|
(mf/defc rol-info
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [member team on-set-admin on-set-editor on-set-owner on-set-viewer profile]}]
|
[{:keys [member team on-set-admin on-set-editor on-set-owner on-set-viewer profile]}]
|
||||||
(let [member-is-owner (:is-owner member)
|
(let [member-is-owner (:is-owner member)
|
||||||
member-is-admin (and (:is-admin member) (not member-is-owner))
|
member-is-admin (and (:is-admin member) (not member-is-owner))
|
||||||
|
@ -309,7 +314,7 @@
|
||||||
(tr "labels.owner")])]]]))
|
(tr "labels.owner")])]]]))
|
||||||
|
|
||||||
(mf/defc member-actions
|
(mf/defc member-actions
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [member team on-delete on-leave profile]}]
|
[{:keys [member team on-delete on-leave profile]}]
|
||||||
(let [is-owner? (:is-owner member)
|
(let [is-owner? (:is-owner member)
|
||||||
owner? (dm/get-in team [:permissions :is-owner])
|
owner? (dm/get-in team [:permissions :is-owner])
|
||||||
|
@ -345,7 +350,7 @@
|
||||||
|
|
||||||
(mf/defc team-member
|
(mf/defc team-member
|
||||||
{::mf/wrap [mf/memo]
|
{::mf/wrap [mf/memo]
|
||||||
::mf/wrap-props false}
|
::mf/props :obj}
|
||||||
[{:keys [team member members profile]}]
|
[{:keys [team member members profile]}]
|
||||||
|
|
||||||
(let [member-id (:id member)
|
(let [member-id (:id member)
|
||||||
|
@ -479,7 +484,7 @@
|
||||||
:on-leave on-leave'}]]]))
|
:on-leave on-leave'}]]]))
|
||||||
|
|
||||||
(mf/defc team-members
|
(mf/defc team-members
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [members-map team profile]}]
|
[{:keys [members-map team profile]}]
|
||||||
(let [members (mf/with-memo [members-map]
|
(let [members (mf/with-memo [members-map]
|
||||||
(->> (vals members-map)
|
(->> (vals members-map)
|
||||||
|
@ -510,8 +515,8 @@
|
||||||
:members members-map}])]]))
|
:members members-map}])]]))
|
||||||
|
|
||||||
(mf/defc team-members-page
|
(mf/defc team-members-page
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [team profile invite-email]}]
|
[{:keys [team profile]}]
|
||||||
(let [members-map (mf/deref refs/dashboard-team-members)]
|
(let [members-map (mf/deref refs/dashboard-team-members)]
|
||||||
|
|
||||||
(mf/with-effect [team]
|
(mf/with-effect [team]
|
||||||
|
@ -525,7 +530,7 @@
|
||||||
(st/emit! (dd/fetch-team-members (:id team))))
|
(st/emit! (dd/fetch-team-members (:id team))))
|
||||||
|
|
||||||
[:*
|
[:*
|
||||||
[:& header {:section :dashboard-team-members :team team :invite-email invite-email}]
|
[:& header {:section :dashboard-team-members :team team}]
|
||||||
[:section {:class (stl/css :dashboard-container :dashboard-team-members)}
|
[:section {:class (stl/css :dashboard-container :dashboard-team-members)}
|
||||||
[:& team-members
|
[:& team-members
|
||||||
{:profile profile
|
{:profile profile
|
||||||
|
@ -536,9 +541,9 @@
|
||||||
;; INVITATIONS SECTION
|
;; INVITATIONS SECTION
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(mf/defc invitation-role-selector
|
(mf/defc invitation-role-selector*
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [can-invite? role status on-change]}]
|
[{:keys [can-invite role status on-change]}]
|
||||||
(let [show? (mf/use-state false)
|
(let [show? (mf/use-state false)
|
||||||
label (cond
|
label (cond
|
||||||
(= role :owner) (tr "labels.owner")
|
(= role :owner) (tr "labels.owner")
|
||||||
|
@ -559,7 +564,7 @@
|
||||||
(on-change role event))))]
|
(on-change role event))))]
|
||||||
|
|
||||||
[:*
|
[:*
|
||||||
(if (and can-invite? (= status :pending))
|
(if (and can-invite (= status :pending))
|
||||||
[:div {:class (stl/css :rol-selector :has-priv)
|
[:div {:class (stl/css :rol-selector :has-priv)
|
||||||
:on-click on-show}
|
:on-click on-show}
|
||||||
[:span {:class (stl/css :rol-label)} label]
|
[:span {:class (stl/css :rol-label)} label]
|
||||||
|
@ -583,7 +588,7 @@
|
||||||
(tr "labels.viewer")]]]]))
|
(tr "labels.viewer")]]]]))
|
||||||
|
|
||||||
(mf/defc invitation-actions
|
(mf/defc invitation-actions
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [invitation team-id]}]
|
[{:keys [invitation team-id]}]
|
||||||
(let [show? (mf/use-state false)
|
(let [show? (mf/use-state false)
|
||||||
|
|
||||||
|
@ -678,10 +683,11 @@
|
||||||
:class (stl/css :action-dropdown-item)}
|
:class (stl/css :action-dropdown-item)}
|
||||||
(tr "labels.delete-invitation")]]]]))
|
(tr "labels.delete-invitation")]]]]))
|
||||||
|
|
||||||
(mf/defc invitation-row
|
(mf/defc invitation-row*
|
||||||
{::mf/wrap [mf/memo]
|
{::mf/wrap [mf/memo]
|
||||||
::mf/wrap-props false}
|
::mf/private true
|
||||||
[{:keys [invitation can-invite? team-id] :as props}]
|
::mf/props :obj}
|
||||||
|
[{:keys [invitation can-invite team-id]}]
|
||||||
|
|
||||||
(let [expired? (:expired invitation)
|
(let [expired? (:expired invitation)
|
||||||
email (:email invitation)
|
email (:email invitation)
|
||||||
|
@ -704,8 +710,8 @@
|
||||||
[:div {:class (stl/css :table-field :field-email)} email]
|
[:div {:class (stl/css :table-field :field-email)} email]
|
||||||
|
|
||||||
[:div {:class (stl/css :table-field :field-roles)}
|
[:div {:class (stl/css :table-field :field-roles)}
|
||||||
[:& invitation-role-selector
|
[:> invitation-role-selector*
|
||||||
{:can-invite? can-invite?
|
{:can-invite can-invite
|
||||||
:role role
|
:role role
|
||||||
:status status
|
:status status
|
||||||
:on-change on-change-role}]]
|
:on-change on-change-role}]]
|
||||||
|
@ -714,16 +720,16 @@
|
||||||
[:& badge-notification {:type type :content badge-content}]]
|
[:& badge-notification {:type type :content badge-content}]]
|
||||||
|
|
||||||
[:div {:class (stl/css :table-field :field-actions)}
|
[:div {:class (stl/css :table-field :field-actions)}
|
||||||
(when can-invite?
|
(when can-invite
|
||||||
[:& invitation-actions
|
[:& invitation-actions
|
||||||
{:invitation invitation
|
{:invitation invitation
|
||||||
:team-id team-id}])]]))
|
:team-id team-id}])]]))
|
||||||
|
|
||||||
(mf/defc empty-invitation-table
|
(mf/defc empty-invitation-table
|
||||||
[{:keys [can-invite?] :as props}]
|
[{:keys [can-invite] :as props}]
|
||||||
[:div {:class (stl/css :empty-invitations)}
|
[:div {:class (stl/css :empty-invitations)}
|
||||||
[:span (tr "labels.no-invitations")]
|
[:span (tr "labels.no-invitations")]
|
||||||
(when can-invite?
|
(when can-invite
|
||||||
[:> i18n/tr-html* {:content (tr "labels.no-invitations-hint")
|
[:> i18n/tr-html* {:content (tr "labels.no-invitations-hint")
|
||||||
:tag-name "span"}])])
|
:tag-name "span"}])])
|
||||||
|
|
||||||
|
@ -731,7 +737,7 @@
|
||||||
[{:keys [team invitations] :as props}]
|
[{:keys [team invitations] :as props}]
|
||||||
(let [owner? (dm/get-in team [:permissions :is-owner])
|
(let [owner? (dm/get-in team [:permissions :is-owner])
|
||||||
admin? (dm/get-in team [:permissions :is-admin])
|
admin? (dm/get-in team [:permissions :is-admin])
|
||||||
can-invite? (or owner? admin?)
|
can-invite (or owner? admin?)
|
||||||
team-id (:id team)]
|
team-id (:id team)]
|
||||||
|
|
||||||
[:div {:class (stl/css :invitations)}
|
[:div {:class (stl/css :invitations)}
|
||||||
|
@ -740,17 +746,17 @@
|
||||||
[:div {:class (stl/css :title-field-role)} (tr "labels.role")]
|
[:div {:class (stl/css :title-field-role)} (tr "labels.role")]
|
||||||
[:div {:class (stl/css :title-field-status)} (tr "labels.status")]]
|
[:div {:class (stl/css :title-field-status)} (tr "labels.status")]]
|
||||||
(if (empty? invitations)
|
(if (empty? invitations)
|
||||||
[:& empty-invitation-table {:can-invite? can-invite?}]
|
[:& empty-invitation-table {:can-invite can-invite}]
|
||||||
[:div {:class (stl/css :table-rows)}
|
[:div {:class (stl/css :table-rows)}
|
||||||
(for [invitation invitations]
|
(for [invitation invitations]
|
||||||
[:& invitation-row
|
[:> invitation-row*
|
||||||
{:key (:email invitation)
|
{:key (:email invitation)
|
||||||
:invitation invitation
|
:invitation invitation
|
||||||
:can-invite? can-invite?
|
:can-invite can-invite
|
||||||
:team-id team-id}])])]))
|
:team-id team-id}])])]))
|
||||||
|
|
||||||
(mf/defc team-invitations-page
|
(mf/defc team-invitations-page
|
||||||
[{:keys [team] :as props}]
|
[{:keys [team]}]
|
||||||
(let [invitations (mf/deref refs/dashboard-team-invitations)]
|
(let [invitations (mf/deref refs/dashboard-team-invitations)]
|
||||||
|
|
||||||
(mf/with-effect [team]
|
(mf/with-effect [team]
|
||||||
|
@ -794,7 +800,7 @@
|
||||||
(mf/defc webhook-modal
|
(mf/defc webhook-modal
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
::mf/register-as :webhook}
|
::mf/register-as :webhook}
|
||||||
[{:keys [webhook] :as props}]
|
[{:keys [webhook]}]
|
||||||
|
|
||||||
(let [initial (mf/with-memo []
|
(let [initial (mf/with-memo []
|
||||||
(or (some-> webhook (update :uri str))
|
(or (some-> webhook (update :uri str))
|
||||||
|
@ -905,7 +911,7 @@
|
||||||
(tr "modals.create-webhook.submit-label"))}]]]]]]))
|
(tr "modals.create-webhook.submit-label"))}]]]]]]))
|
||||||
|
|
||||||
(mf/defc webhooks-hero
|
(mf/defc webhooks-hero
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[]
|
[]
|
||||||
[:div {:class (stl/css :webhooks-hero-container)}
|
[:div {:class (stl/css :webhooks-hero-container)}
|
||||||
[:h2 {:class (stl/css :hero-title)}
|
[:h2 {:class (stl/css :hero-title)}
|
||||||
|
@ -939,18 +945,9 @@
|
||||||
:class (stl/css :menu-disabled)}
|
:class (stl/css :menu-disabled)}
|
||||||
[:> icon* {:id "menu"}]])))
|
[:> icon* {:id "menu"}]])))
|
||||||
|
|
||||||
(mf/defc last-delivery-icon
|
|
||||||
{::mf/wrap-props false}
|
|
||||||
[{:keys [success? text]}]
|
|
||||||
[:div {:class (stl/css :last-delivery-icon)
|
|
||||||
:title text}
|
|
||||||
(if success?
|
|
||||||
success-icon
|
|
||||||
warning-icon)])
|
|
||||||
|
|
||||||
(mf/defc webhook-item
|
(mf/defc webhook-item
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
[{:keys [webhook permissions] :as props}]
|
[{:keys [webhook permissions]}]
|
||||||
(let [error-code (:error-code webhook)
|
(let [error-code (:error-code webhook)
|
||||||
id (:id webhook)
|
id (:id webhook)
|
||||||
creator-id (:profile-id webhook)
|
creator-id (:profile-id webhook)
|
||||||
|
@ -1014,14 +1011,14 @@
|
||||||
:can-edit can-edit}]]]))
|
:can-edit can-edit}]]]))
|
||||||
|
|
||||||
(mf/defc webhooks-list
|
(mf/defc webhooks-list
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [webhooks permissions]}]
|
[{:keys [webhooks permissions]}]
|
||||||
[:div {:class (stl/css :table-rows :webhook-table)}
|
[:div {:class (stl/css :table-rows :webhook-table)}
|
||||||
(for [webhook webhooks]
|
(for [webhook webhooks]
|
||||||
[:& webhook-item {:webhook webhook :key (:id webhook) :permissions permissions}])])
|
[:& webhook-item {:webhook webhook :key (:id webhook) :permissions permissions}])])
|
||||||
|
|
||||||
(mf/defc team-webhooks-page
|
(mf/defc team-webhooks-page
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [team]}]
|
[{:keys [team]}]
|
||||||
(let [webhooks (mf/deref refs/dashboard-team-webhooks)]
|
(let [webhooks (mf/deref refs/dashboard-team-webhooks)]
|
||||||
|
|
||||||
|
@ -1051,7 +1048,7 @@
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(mf/defc team-settings-page
|
(mf/defc team-settings-page
|
||||||
{::mf/wrap-props false}
|
{::mf/props :obj}
|
||||||
[{:keys [team]}]
|
[{:keys [team]}]
|
||||||
(let [finput (mf/use-ref)
|
(let [finput (mf/use-ref)
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,8 @@
|
||||||
|
|
||||||
(defn- create-form-mutator
|
(defn- create-form-mutator
|
||||||
[internal-state rerender-fn wrap-update-fn initial opts]
|
[internal-state rerender-fn wrap-update-fn initial opts]
|
||||||
|
(mf/set-ref-val! internal-state initial)
|
||||||
|
|
||||||
(reify
|
(reify
|
||||||
IDeref
|
IDeref
|
||||||
(-deref [_]
|
(-deref [_]
|
||||||
|
@ -128,6 +130,12 @@
|
||||||
[& {:keys [initial] :as opts}]
|
[& {:keys [initial] :as opts}]
|
||||||
(let [rerender-fn (use-rerender-fn)
|
(let [rerender-fn (use-rerender-fn)
|
||||||
|
|
||||||
|
initial
|
||||||
|
(mf/with-memo [initial]
|
||||||
|
{:data (if (fn? initial) (initial) initial)
|
||||||
|
:errors {}
|
||||||
|
:touched {}})
|
||||||
|
|
||||||
internal-state
|
internal-state
|
||||||
(mf/use-ref nil)
|
(mf/use-ref nil)
|
||||||
|
|
||||||
|
@ -136,11 +144,8 @@
|
||||||
(create-form-mutator internal-state rerender-fn wrap-update-schema-fn initial opts))]
|
(create-form-mutator internal-state rerender-fn wrap-update-schema-fn initial opts))]
|
||||||
|
|
||||||
;; Initialize internal state once
|
;; Initialize internal state once
|
||||||
(mf/with-effect []
|
(mf/with-layout-effect []
|
||||||
(mf/set-ref-val! internal-state
|
(mf/set-ref-val! internal-state initial))
|
||||||
{:data {}
|
|
||||||
:errors {}
|
|
||||||
:touched {}}))
|
|
||||||
|
|
||||||
(mf/with-effect [initial]
|
(mf/with-effect [initial]
|
||||||
(if (fn? initial)
|
(if (fn? initial)
|
||||||
|
|
|
@ -421,7 +421,7 @@ msgid "dashboard.download-binary-file"
|
||||||
msgstr "Download Penpot file (.penpot)"
|
msgstr "Download Penpot file (.penpot)"
|
||||||
|
|
||||||
msgid "dashboard.download-binary-file-v3"
|
msgid "dashboard.download-binary-file-v3"
|
||||||
msgstr "Download Penpot file (.zip) (BETA)"
|
msgstr "Download (.zip)"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/file_menu.cljs:300, src/app/main/ui/workspace/main_menu.cljs:597
|
#: src/app/main/ui/dashboard/file_menu.cljs:300, src/app/main/ui/workspace/main_menu.cljs:597
|
||||||
msgid "dashboard.download-standard-file"
|
msgid "dashboard.download-standard-file"
|
||||||
|
|
Loading…
Reference in a new issue