From c570557203323f75a4dd131e000ccf7b75a84a77 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 13 Dec 2022 09:52:30 +0100 Subject: [PATCH] :recycle: Move teams queries and mutations to commands --- backend/src/app/rpc.clj | 1 + backend/src/app/rpc/commands/auth.clj | 2 +- backend/src/app/rpc/commands/comments.clj | 2 +- backend/src/app/rpc/commands/files.clj | 2 +- backend/src/app/rpc/commands/management.clj | 3 +- backend/src/app/rpc/commands/teams.clj | 817 ++++++++++++++++++ backend/src/app/rpc/commands/verify_token.clj | 2 +- backend/src/app/rpc/commands/webhooks.clj | 2 +- backend/src/app/rpc/mutations/fonts.clj | 2 +- backend/src/app/rpc/mutations/media.clj | 2 +- backend/src/app/rpc/mutations/profile.clj | 12 +- backend/src/app/rpc/mutations/projects.clj | 35 +- backend/src/app/rpc/mutations/teams.clj | 412 ++------- backend/src/app/rpc/queries/comments.clj | 2 +- backend/src/app/rpc/queries/files.clj | 68 +- backend/src/app/rpc/queries/fonts.clj | 3 +- backend/src/app/rpc/queries/projects.clj | 2 +- backend/src/app/rpc/queries/teams.clj | 228 +---- backend/test/backend_tests/helpers.clj | 9 +- frontend/src/app/main/data/dashboard.cljs | 29 +- frontend/src/app/main/data/users.cljs | 4 +- frontend/src/app/main/data/workspace.cljs | 2 +- frontend/src/app/main/repo.cljs | 5 + 23 files changed, 993 insertions(+), 653 deletions(-) create mode 100644 backend/src/app/rpc/commands/teams.clj diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 849f8370c..61c59a58a 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -268,6 +268,7 @@ 'app.rpc.commands.management 'app.rpc.commands.verify-token 'app.rpc.commands.search + 'app.rpc.commands.teams 'app.rpc.commands.auth 'app.rpc.commands.ldap 'app.rpc.commands.demo diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index d34a55ab2..9ad8cbf1e 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -16,9 +16,9 @@ [app.http.session :as session] [app.loggers.audit :as audit] [app.rpc.climit :as climit] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.mutations.teams :as teams] [app.rpc.queries.profile :as profile] [app.tokens :as tokens] [app.util.services :as sv] diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index f2aad072d..45d83557e 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -12,8 +12,8 @@ [app.db :as db] [app.loggers.webhooks :as-alias webhooks] [app.rpc.commands.files :as files] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] - [app.rpc.queries.teams :as teams] [app.rpc.retry :as retry] [app.util.blob :as blob] [app.util.services :as sv] diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 27df59f4e..98518feb9 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -19,13 +19,13 @@ [app.db.sql :as sql] [app.loggers.webhooks :as-alias webhooks] [app.rpc.commands.files.thumbnails :as-alias thumbs] + [app.rpc.commands.teams :as teams] [app.rpc.cond :as-alias cond] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.permissions :as perms] [app.rpc.queries.projects :as projects] [app.rpc.queries.share-link :refer [retrieve-share-link]] - [app.rpc.queries.teams :as teams] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.services :as sv] diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 7e56bec6f..d647c90c4 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -15,10 +15,9 @@ [app.db :as db] [app.rpc.commands.binfile :as binfile] [app.rpc.commands.files :as files] + [app.rpc.commands.teams :as teams :refer [create-project-role create-project]] [app.rpc.doc :as-alias doc] - [app.rpc.mutations.projects :refer [create-project-role create-project]] [app.rpc.queries.projects :as proj] - [app.rpc.queries.teams :as teams] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.services :as sv] diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj new file mode 100644 index 000000000..ec88484e0 --- /dev/null +++ b/backend/src/app/rpc/commands/teams.clj @@ -0,0 +1,817 @@ +;; 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 + (:require + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.emails :as eml] + [app.loggers.audit :as audit] + [app.media :as media] + [app.rpc.climit :as climit] + [app.rpc.doc :as-alias doc] + [app.rpc.helpers :as rph] + [app.rpc.permissions :as perms] + [app.rpc.queries.profile :as profile] + [app.storage :as sto] + [app.tokens :as tokens] + [app.util.services :as sv] + [app.util.time :as dt] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [promesa.core :as p] + [promesa.exec :as px])) + +;; --- Helpers & Specs + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::profile-id ::us/uuid) +(s/def ::file-id ::us/uuid) +(s/def ::team-id ::us/uuid) + +(def ^:private sql:team-permissions + "select tpr.is_owner, + tpr.is_admin, + tpr.can_edit + from team_profile_rel as tpr + join team as t on (t.id = tpr.team_id) + where tpr.profile_id = ? + and tpr.team_id = ? + and t.deleted_at is null") + +(defn get-permissions + [conn profile-id team-id] + (let [rows (db/exec! conn [sql:team-permissions profile-id team-id]) + is-owner (boolean (some :is-owner rows)) + is-admin (boolean (some :is-admin rows)) + can-edit (boolean (some :can-edit rows))] + (when (seq rows) + {:is-owner is-owner + :is-admin (or is-owner is-admin) + :can-edit (or is-owner is-admin can-edit) + :can-read true}))) + +(def has-edit-permissions? + (perms/make-edition-predicate-fn get-permissions)) + +(def has-read-permissions? + (perms/make-read-predicate-fn get-permissions)) + +(def check-edition-permissions! + (perms/make-check-fn has-edit-permissions?)) + +(def check-read-permissions! + (perms/make-check-fn has-read-permissions?)) + +;; --- Query: Teams + +(declare retrieve-teams) + +(s/def ::get-teams + (s/keys :req-un [::profile-id])) + +(sv/defmethod ::get-teams + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [profile-id]}] + (with-open [conn (db/open pool)] + (retrieve-teams conn profile-id))) + +(def sql:teams + "select t.*, + tp.is_owner, + tp.is_admin, + tp.can_edit, + (t.id = ?) as is_default + from team_profile_rel as tp + join team as t on (t.id = tp.team_id) + where t.deleted_at is null + and tp.profile_id = ? + order by tp.created_at asc") + +(defn process-permissions + [team] + (let [is-owner (:is-owner team) + is-admin (:is-admin team) + can-edit (:can-edit team) + permissions {:type :membership + :is-owner is-owner + :is-admin (or is-owner is-admin) + :can-edit (or is-owner is-admin can-edit)}] + (-> team + (dissoc :is-owner :is-admin :can-edit) + (assoc :permissions permissions)))) + +(defn retrieve-teams + [conn profile-id] + (let [defaults (profile/retrieve-additional-data conn profile-id)] + (->> (db/exec! conn [sql:teams (:default-team-id defaults) profile-id]) + (mapv process-permissions)))) + +;; --- Query: Team (by ID) + +(declare retrieve-team) + +(s/def ::get-team + (s/keys :req-un [::profile-id ::id])) + +(sv/defmethod ::get-team + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [profile-id id]}] + (with-open [conn (db/open pool)] + (retrieve-team conn profile-id id))) + +(defn retrieve-team + [conn profile-id team-id] + (let [defaults (profile/retrieve-additional-data conn profile-id) + sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?") + result (db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id])] + (when-not result + (ex/raise :type :not-found + :code :team-does-not-exist)) + (process-permissions result))) + + +;; --- Query: Team Members + +(def sql:team-members + "select tp.*, + p.id, + p.email, + p.fullname as name, + p.fullname as fullname, + p.photo_id, + p.is_active + from team_profile_rel as tp + join profile as p on (p.id = tp.profile_id) + where tp.team_id = ?") + +(defn retrieve-team-members + [conn team-id] + (db/exec! conn [sql:team-members team-id])) + +(s/def ::team-id ::us/uuid) +(s/def ::get-team-members + (s/keys :req-un [::profile-id ::team-id])) + +(sv/defmethod ::get-team-members + {::doc/added "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id]}] + (with-open [conn (db/open pool)] + (check-read-permissions! conn profile-id team-id) + (retrieve-team-members conn team-id))) + + +;; --- Query: Team Users + +(declare retrieve-users) +(declare retrieve-team-for-file) + +(s/def ::get-team-users + (s/and (s/keys :req-un [::profile-id] + :opt-un [::team-id ::file-id]) + #(or (:team-id %) (:file-id %)))) + +(sv/defmethod ::get-team-users + {::doc/added "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id file-id]}] + (with-open [conn (db/open pool)] + (if team-id + (do + (check-read-permissions! conn profile-id team-id) + (retrieve-users conn team-id)) + (let [{team-id :id} (retrieve-team-for-file conn file-id)] + (check-read-permissions! conn profile-id team-id) + (retrieve-users conn team-id))))) + +;; This is a similar query to team members but can contain more data +;; because some user can be explicitly added to project or file (not +;; implemented in UI) + +(def sql:team-users + "select pf.id, pf.fullname, pf.photo_id + from profile as pf + inner join team_profile_rel as tpr on (tpr.profile_id = pf.id) + where tpr.team_id = ? + union + select pf.id, pf.fullname, pf.photo_id + from profile as pf + inner join project_profile_rel as ppr on (ppr.profile_id = pf.id) + inner join project as p on (ppr.project_id = p.id) + where p.team_id = ? + union + select pf.id, pf.fullname, pf.photo_id + from profile as pf + inner join file_profile_rel as fpr on (fpr.profile_id = pf.id) + inner join file as f on (fpr.file_id = f.id) + inner join project as p on (f.project_id = p.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 retrieve-users + [conn team-id] + (db/exec! conn [sql:team-users team-id team-id team-id])) + +(defn retrieve-team-for-file + [conn file-id] + (->> [sql:team-by-file file-id] + (db/exec-one! conn))) + +;; --- Query: Team Stats + +(declare retrieve-team-stats) + +(s/def ::get-team-stats + (s/keys :req-un [::profile-id ::team-id])) + +(sv/defmethod ::get-team-stats + {::doc/added "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id]}] + (with-open [conn (db/open pool)] + (check-read-permissions! conn profile-id team-id) + (retrieve-team-stats conn team-id))) + +(def sql:team-stats + "select (select count(*) from project where team_id = ?) as projects, + (select count(*) from file as f join project as p on (p.id = f.project_id) where p.team_id = ?) as files") + +(defn retrieve-team-stats + [conn team-id] + (db/exec-one! conn [sql:team-stats team-id team-id])) + + +;; --- Query: Team invitations + +(s/def ::get-team-invitations + (s/keys :req-un [::profile-id ::team-id])) + +(def sql:team-invitations + "select email_to as email, role, (valid_until < now()) as expired + from team_invitation where team_id = ? order by valid_until desc") + +(defn get-team-invitations + [conn team-id] + (->> (db/exec! conn [sql:team-invitations team-id]) + (mapv #(update % :role keyword)))) + +(sv/defmethod ::get-team-invitations + {::doc/added "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id]}] + (with-open [conn (db/open pool)] + (check-read-permissions! conn profile-id team-id) + (get-team-invitations conn team-id))) + +;; --- Mutation: Create Team + +(declare create-team) +(declare create-project) +(declare create-project-role) +(declare ^:private create-team*) +(declare ^:private create-team-role) +(declare ^:private create-team-default-project) + +(s/def ::create-team + (s/keys :req-un [::profile-id ::name] + :opt-un [::id])) + +(sv/defmethod ::create-team + {::doc/added "1.17"} + [{:keys [::db/pool] :as cfg} params] + (db/with-atomic [conn pool] + (create-team conn params))) + +(defn create-team + "This is a complete team creation process, it creates the team + object and all related objects (default role and default project)." + [conn params] + (let [team (create-team* conn params) + params (assoc params + :team-id (:id team) + :role :owner) + project (create-team-default-project conn params)] + (create-team-role conn params) + (assoc team :default-project-id (:id project)))) + +(defn- create-team* + [conn {:keys [id name is-default] :as params}] + (let [id (or id (uuid/next)) + is-default (if (boolean? is-default) is-default false)] + (db/insert! conn :team + {:id id + :name name + :is-default is-default}))) + +(defn- create-team-role + [conn {:keys [team-id profile-id role] :as params}] + (let [params {:team-id team-id + :profile-id profile-id}] + (->> (perms/assign-role-flags params role) + (db/insert! conn :team-profile-rel)))) + +(defn- create-team-default-project + [conn {:keys [team-id profile-id] :as params}] + (let [project {:id (uuid/next) + :team-id team-id + :name "Drafts" + :is-default true} + project (create-project conn project)] + (create-project-role conn {:project-id (:id project) + :profile-id profile-id + :role :owner}) + project)) + +;; NOTE: we have project creation here because there are cyclic +;; dependency between teams and projects namespaces, and the project +;; creation happens in both sides, on team creation and on simple +;; project creation, so it make sense to have this functions in this +;; namespace too. + +(defn create-project + [conn {:keys [id team-id name is-default] :as params}] + (let [id (or id (uuid/next)) + is-default (if (boolean? is-default) is-default false)] + (db/insert! conn :project + {:id id + :name name + :team-id team-id + :is-default is-default}))) + +(defn create-project-role + [conn {:keys [project-id profile-id role]}] + (let [params {:project-id project-id + :profile-id profile-id}] + (->> (perms/assign-role-flags params role) + (db/insert! conn :project-profile-rel)))) + +;; --- Mutation: Update Team + +(s/def ::update-team + (s/keys :req-un [::profile-id ::name ::id])) + +(sv/defmethod ::update-team + {::doc/added "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [id name profile-id] :as params}] + (db/with-atomic [conn pool] + (check-edition-permissions! conn profile-id id) + (db/update! conn :team + {:name name} + {:id id}) + nil)) + + +;; --- Mutation: Leave Team + +(declare role->params) + +(s/def ::reassign-to ::us/uuid) +(s/def ::leave-team + (s/keys :req-un [::profile-id ::id] + :opt-un [::reassign-to])) + +(defn leave-team + [conn {:keys [id profile-id reassign-to]}] + (let [perms (get-permissions conn profile-id id) + members (retrieve-team-members conn id)] + + (cond + ;; we can only proceed if there are more members in the team + ;; besides the current profile + (<= (count members) 1) + (ex/raise :type :validation + :code :no-enough-members-for-leave + :context {:members (count members)}) + + ;; if the `reassign-to` is filled and has a different value + ;; than the current profile-id, we proceed to reassing the + ;; owner role to profile identified by the `reassign-to`. + (and reassign-to (not= reassign-to profile-id)) + (let [member (d/seek #(= reassign-to (:id %)) members)] + (when-not member + (ex/raise :type :not-found :code :member-does-not-exist)) + + ;; unasign owner role to current profile + (db/update! conn :team-profile-rel + {:is-owner false} + {:team-id id + :profile-id profile-id}) + + ;; assign owner role to new profile + (db/update! conn :team-profile-rel + (role->params :owner) + {:team-id id :profile-id reassign-to})) + + ;; and finally, if all other conditions does not match and the + ;; current profile is owner, we dont allow it because there + ;; must always be an owner. + (:is-owner perms) + (ex/raise :type :validation + :code :owner-cant-leave-team + :hint "releasing owner before leave")) + + (db/delete! conn :team-profile-rel + {:profile-id profile-id + :team-id id}) + + nil)) + + +(sv/defmethod ::leave-team + {::doc/added "1.17"} + [{:keys [pool] :as cfg} params] + (db/with-atomic [conn pool] + (leave-team conn params))) + +;; --- Mutation: Delete Team + +(s/def ::delete-team + (s/keys :req-un [::profile-id ::id])) + +;; TODO: right now just don't allow delete default team, in future it +;; should raise a specific exception for signal that this action is +;; not allowed. + +(sv/defmethod ::delete-team + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] + (db/with-atomic [conn pool] + (let [perms (get-permissions conn profile-id id)] + (when-not (:is-owner perms) + (ex/raise :type :validation + :code :only-owner-can-delete-team)) + + (db/update! conn :team + {:deleted-at (dt/now)} + {:id id :is-default false}) + nil))) + + +;; --- Mutation: Team Update Role + +(s/def ::team-id ::us/uuid) +(s/def ::member-id ::us/uuid) +;; Temporarily disabled viewer role +;; https://tree.taiga.io/project/uxboxproject/issue/1083 +;; (s/def ::role #{:owner :admin :editor :viewer}) +(s/def ::role #{:owner :admin :editor}) + +(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 + [conn {:keys [team-id profile-id member-id role] :as params}] + ;; We retrieve all team members instead of query the + ;; database for a single member. This is just for + ;; convenience, if this becomes a bottleneck or problematic, + ;; we will change it to more efficient fetch mechanisms. + (let [perms (get-permissions conn profile-id team-id) + members (retrieve-team-members conn team-id) + member (d/seek #(= member-id (:id %)) members) + + is-owner? (:is-owner perms) + is-admin? (:is-admin perms)] + + ;; If no member is found, just 404 + (when-not member + (ex/raise :type :not-found + :code :member-does-not-exist)) + + ;; First check if we have permissions to change roles + (when-not (or is-owner? is-admin?) + (ex/raise :type :validation + :code :insufficient-permissions)) + + ;; Don't allow change role of owner member + (when (:is-owner member) + (ex/raise :type :validation + :code :cant-change-role-to-owner)) + + ;; Don't allow promote to owner to admin users. + (when (and (not is-owner?) (= role :owner)) + (ex/raise :type :validation + :code :cant-promote-to-owner)) + + (let [params (role->params role)] + ;; Only allow single owner on team + (when (= role :owner) + (db/update! conn :team-profile-rel + {:is-owner false} + {:team-id team-id + :profile-id profile-id})) + + (db/update! conn :team-profile-rel + params + {:team-id team-id + :profile-id member-id}) + nil))) + +(s/def ::update-team-member-role + (s/keys :req-un [::profile-id ::team-id ::member-id ::role])) + +(sv/defmethod ::update-team-member-role + {::doc/added "1.17"} + [{:keys [::db/pool] :as cfg} params] + (db/with-atomic [conn pool] + (update-team-member-role conn params))) + + +;; --- Mutation: Delete Team Member + +(s/def ::delete-team-member + (s/keys :req-un [::profile-id ::team-id ::member-id])) + +(sv/defmethod ::delete-team-member + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [team-id profile-id member-id] :as params}] + (db/with-atomic [conn pool] + (let [perms (get-permissions conn profile-id team-id)] + (when-not (or (:is-owner perms) + (:is-admin perms)) + (ex/raise :type :validation + :code :insufficient-permissions)) + + (when (= member-id profile-id) + (ex/raise :type :validation + :code :cant-remove-yourself)) + + (db/delete! conn :team-profile-rel {:profile-id member-id + :team-id team-id}) + + nil))) + +;; --- Mutation: Update Team Photo + +(declare ^:private upload-photo) +(declare ^:private update-team-photo) + +(s/def ::file ::media/upload) +(s/def ::update-team-photo + (s/keys :req-un [::profile-id ::team-id ::file])) + +(sv/defmethod ::update-team-photo + {::doc/added "1.17"} + [cfg {:keys [file] :as params}] + ;; Validate incoming mime type + (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) + (let [cfg (update cfg :storage media/configure-assets-storage)] + (update-team-photo cfg params))) + +(defn update-team-photo + [{:keys [pool storage executor] :as cfg} {:keys [profile-id team-id] :as params}] + (p/let [team (px/with-dispatch executor + (retrieve-team pool profile-id team-id)) + photo (upload-photo cfg params)] + + ;; Mark object as touched for make it ellegible for tentative + ;; garbage collection. + (when-let [id (:photo-id team)] + (sto/touch-object! storage id)) + + ;; Save new photo + (db/update! pool :team + {:photo-id (:id photo)} + {:id team-id}) + + (assoc team :photo-id (:id photo)))) + +(defn upload-photo + [{:keys [storage executor climit] :as cfg} {:keys [file]}] + (letfn [(get-info [content] + (climit/with-dispatch (:process-image climit) + (media/run {:cmd :info :input content}))) + + (generate-thumbnail [info] + (climit/with-dispatch (:process-image climit) + (media/run {:cmd :profile-thumbnail + :format :jpeg + :quality 85 + :width 256 + :height 256 + :input info}))) + + ;; Function responsible of calculating cryptographyc hash of + ;; the provided data. + (calculate-hash [data] + (px/with-dispatch executor + (sto/calculate-hash data)))] + + (p/let [info (get-info file) + thumb (generate-thumbnail info) + hash (calculate-hash (:data thumb)) + content (-> (sto/content (:data thumb) (:size thumb)) + (sto/wrap-with-hash hash))] + (sto/put-object! storage {::sto/content content + ::sto/deduplicate? true + :bucket "profile" + :content-type (:mtype thumb)})))) + +;; --- Mutation: Create Team Invitation + +(def sql:upsert-team-invitation + "insert into team_invitation(team_id, email_to, role, valid_until) + values (?, ?, ?, ?) + on conflict(team_id, email_to) do + update set role = ?, valid_until = ?, updated_at = now();") + +(defn- create-invitation + [{:keys [conn sprops team profile role email] :as cfg}] + (let [member (profile/retrieve-profile-data-by-email conn email) + token-exp (dt/in-future "168h") ;; 7 days + email (str/lower email) + itoken (tokens/generate sprops + {:iss :team-invitation + :exp token-exp + :profile-id (:id profile) + :role role + :team-id (:id team) + :member-email (:email member email) + :member-id (:id member)}) + ptoken (tokens/generate sprops + {:iss :profile-identity + :profile-id (:id profile) + :exp (dt/in-future {:days 30})})] + + (when (and member (not (eml/allow-send-emails? conn member))) + (ex/raise :type :validation + :code :member-is-muted + :email email + :hint "the profile has reported repeatedly as spam or has bounces")) + + ;; Secondly check if the invited member email is part of the global spam/bounce report. + (when (eml/has-bounce-reports? conn email) + (ex/raise :type :validation + :code :email-has-permanent-bounces + :email email + :hint "the email you invite has been repeatedly reported as spam or bounce")) + + (when (contains? cf/flags :log-invitation-tokens) + (l/trace :hint "invitation token" :token itoken)) + + ;; 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 {: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)}))) + (do + (db/exec-one! conn [sql:upsert-team-invitation + (:id team) (str/lower email) (name role) + token-exp (name role) token-exp]) + (eml/send! {::eml/conn conn + ::eml/factory eml/invite-to-team + :public-uri (:public-uri cfg) + :to email + :invited-by (:fullname profile) + :team (:name team) + :token itoken + :extra-data ptoken}))) + + itoken)) + +(s/def ::email ::us/email) +(s/def ::emails ::us/set-of-valid-emails) +(s/def ::create-team-invitations + (s/keys :req-un [::profile-id ::team-id ::role] + :opt-un [::email ::emails])) + +(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"} + [{:keys [pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}] + (db/with-atomic [conn pool] + (let [perms (get-permissions conn profile-id team-id) + profile (db/get-by-id conn :profile profile-id) + team (db/get-by-id conn :team team-id) + emails (cond-> (or emails #{}) (string? email) (conj email))] + + (when-not (:is-admin perms) + (ex/raise :type :validation + :code :insufficient-permissions)) + + ;; First check if the current profile is allowed to send emails. + (when-not (eml/allow-send-emails? conn profile) + (ex/raise :type :validation + :code :profile-is-muted + :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) + + (let [invitations (->> emails + (map (fn [email] + (assoc cfg + :email email + :conn conn + :team team + :profile profile + :role role))) + (map create-invitation))] + (with-meta (vec invitations) + {::audit/props {:invitations (count invitations)}}))))) + + +;; --- Mutation: Create Team & Invite Members + +(s/def ::emails ::us/set-of-valid-emails) +(s/def ::create-team-and-invitations + (s/merge ::create-team + (s/keys :req-un [::emails ::role]))) + +(sv/defmethod ::create-team-and-invitations + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}] + (db/with-atomic [conn pool] + (let [team (create-team conn params) + profile (db/get-by-id conn :profile profile-id)] + + ;; Create invitations for all provided emails. + (doseq [email emails] + (create-invitation + (assoc cfg + :conn conn + :team team + :profile profile + :email email + :role role))) + + (-> team + (vary-meta assoc ::audit/props {:invitations (count emails)}) + (rph/with-defer + #(when-let [collector (::audit/collector cfg)] + (audit/submit! collector + {:type "command" + :name "create-team-invitations" + :profile-id profile-id + :props {:emails emails + :role role + :profile-id profile-id + :invitations (count emails)}}))))))) + +;; --- Mutation: Update invitation role + +(s/def ::update-team-invitation-role + (s/keys :req-un [::profile-id ::team-id ::email ::role])) + +(sv/defmethod ::update-team-invitation-role + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [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 (str/lower email)}) + nil))) + +;; --- Mutation: Delete invitation + +(s/def ::delete-team-invitation + (s/keys :req-un [::profile-id ::team-id ::email])) + +(sv/defmethod ::delete-team-invitation + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [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)) + + (db/delete! conn :team-invitation + {:team-id team-id :email-to (str/lower email)}) + nil))) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 9b5df458f..3242a8211 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -11,9 +11,9 @@ [app.db :as db] [app.http.session :as session] [app.loggers.audit :as audit] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.mutations.teams :as teams] [app.rpc.queries.profile :as profile] [app.tokens :as tokens] [app.tokens.spec.team-invitation :as-alias spec.team-invitation] diff --git a/backend/src/app/rpc/commands/webhooks.clj b/backend/src/app/rpc/commands/webhooks.clj index fdbc30851..454dfee42 100644 --- a/backend/src/app/rpc/commands/webhooks.clj +++ b/backend/src/app/rpc/commands/webhooks.clj @@ -12,8 +12,8 @@ [app.db :as db] [app.http.client :as http] [app.loggers.webhooks :as webhooks] + [app.rpc.commands.teams :refer [check-edition-permissions! check-read-permissions!]] [app.rpc.doc :as-alias doc] - [app.rpc.queries.teams :refer [check-edition-permissions! check-read-permissions!]] [app.util.services :as sv] [app.util.time :as dt] [app.worker :as-alias wrk] diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj index b92a3fc86..716147a39 100644 --- a/backend/src/app/rpc/mutations/fonts.clj +++ b/backend/src/app/rpc/mutations/fonts.clj @@ -15,9 +15,9 @@ [app.loggers.webhooks :as-alias webhooks] [app.media :as media] [app.rpc.climit :as-alias climit] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.teams :as teams] [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index fb02982db..dba890ed4 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -16,7 +16,7 @@ [app.http.client :as http] [app.media :as media] [app.rpc.climit :as climit] - [app.rpc.queries.teams :as teams] + [app.rpc.commands.teams :as teams] [app.storage :as sto] [app.storage.tmp :as tmp] [app.util.services :as sv] diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index c41a3501c..e1f07f598 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -17,10 +17,10 @@ [app.media :as media] [app.rpc :as-alias rpc] [app.rpc.climit :as-alias climit] - [app.rpc.commands.auth :as cmd.auth] + [app.rpc.commands.auth :as auth] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.mutations.teams :as teams] [app.rpc.queries.profile :as profile] [app.storage :as sto] [app.tokens :as tokens] @@ -111,7 +111,7 @@ (defn- validate-password! [conn {:keys [profile-id old-password] :as params}] (let [profile (db/get-by-id conn :profile profile-id)] - (when-not (:valid (cmd.auth/verify-password old-password (:password profile))) + (when-not (:valid (auth/verify-password old-password (:password profile))) (ex/raise :type :validation :code :old-password-not-match)) profile)) @@ -119,7 +119,7 @@ (defn update-profile-password! [conn {:keys [id password] :as profile}] (db/update! conn :profile - {:password (cmd.auth/derive-password password)} + {:password (auth/derive-password password)} {:id id})) ;; --- MUTATION: Update Photo @@ -182,7 +182,7 @@ (defn- change-email-immediately [{:keys [conn]} {:keys [profile email] :as params}] (when (not= email (:email profile)) - (cmd.auth/check-profile-existence! conn params)) + (auth/check-profile-existence! conn params)) (db/update! conn :profile {:email email} {:id (:id profile)}) @@ -201,7 +201,7 @@ :exp (dt/in-future {:days 30})})] (when (not= email (:email profile)) - (cmd.auth/check-profile-existence! conn params)) + (auth/check-profile-existence! conn params)) (when-not (eml/allow-send-emails? conn profile) (ex/raise :type :validation diff --git a/backend/src/app/rpc/mutations/projects.clj b/backend/src/app/rpc/mutations/projects.clj index 95c36d957..ed7a07334 100644 --- a/backend/src/app/rpc/mutations/projects.clj +++ b/backend/src/app/rpc/mutations/projects.clj @@ -7,15 +7,13 @@ (ns app.rpc.mutations.projects (:require [app.common.spec :as us] - [app.common.uuid :as uuid] [app.db :as db] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.permissions :as perms] [app.rpc.queries.projects :as proj] - [app.rpc.queries.teams :as teams] [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s])) @@ -28,9 +26,7 @@ ;; --- Mutation: Create Project -(declare create-project) -(declare create-project-role) -(declare create-team-project-profile) +(declare create-project-profile-state) (s/def ::team-id ::us/uuid) (s/def ::create-project @@ -43,33 +39,15 @@ [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] (db/with-atomic [conn pool] (teams/check-edition-permissions! conn profile-id team-id) - (let [project (create-project conn params) + (let [project (teams/create-project conn params) params (assoc params :project-id (:id project) :role :owner)] - (create-project-role conn params) - (create-team-project-profile conn params) + (teams/create-project-role conn params) + (create-project-profile-state conn params) (assoc project :is-pinned true)))) -(defn create-project - [conn {:keys [id team-id name is-default] :as params}] - (let [id (or id (uuid/next)) - is-default (if (boolean? is-default) is-default false)] - (db/insert! conn :project - {:id id - :name name - :team-id team-id - :is-default is-default}))) - -(defn create-project-role - [conn {:keys [project-id profile-id role]}] - (let [params {:project-id project-id - :profile-id profile-id}] - (->> (perms/assign-role-flags params role) - (db/insert! conn :project-profile-rel)))) - -;; TODO: pending to be refactored -(defn create-team-project-profile +(defn create-project-profile-state [conn {:keys [team-id project-id profile-id] :as params}] (db/insert! conn :team-project-profile-rel {:project-id project-id @@ -77,7 +55,6 @@ :team-id team-id :is-pinned true})) - ;; --- Mutation: Toggle Project Pin (def ^:private diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index 7da456c58..d15a376df 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -6,30 +6,19 @@ (ns app.rpc.mutations.teams (:require - [app.common.data :as d] [app.common.exceptions :as ex] - [app.common.logging :as l] [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.config :as cf] [app.db :as db] [app.emails :as eml] [app.loggers.audit :as audit] [app.media :as media] - [app.rpc.climit :as climit] + [app.rpc.commands.teams :as cmd.teams] + [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.mutations.projects :as projects] - [app.rpc.permissions :as perms] - [app.rpc.queries.profile :as profile] - [app.rpc.queries.teams :as teams] - [app.storage :as sto] - [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s] - [cuerdas.core :as str] - [promesa.core :as p] - [promesa.exec :as px])) + [cuerdas.core :as str])) ;; --- Helpers & Specs @@ -39,148 +28,54 @@ ;; --- Mutation: Create Team -(declare create-team) -(declare create-team-entry) -(declare create-team-role) -(declare create-team-default-project) - -(s/def ::create-team - (s/keys :req-un [::profile-id ::name] - :opt-un [::id])) +(s/def ::create-team ::cmd.teams/create-team) (sv/defmethod ::create-team - [{:keys [pool] :as cfg} params] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] - (create-team conn params))) - -(defn create-team - "This is a complete team creation process, it creates the team - object and all related objects (default role and default project)." - [conn params] - (let [team (create-team-entry conn params) - params (assoc params - :team-id (:id team) - :role :owner) - project (create-team-default-project conn params)] - (create-team-role conn params) - (assoc team :default-project-id (:id project)))) - -(defn- create-team-entry - [conn {:keys [id name is-default] :as params}] - (let [id (or id (uuid/next)) - is-default (if (boolean? is-default) is-default false)] - (db/insert! conn :team - {:id id - :name name - :is-default is-default}))) - -(defn- create-team-role - [conn {:keys [team-id profile-id role] :as params}] - (let [params {:team-id team-id - :profile-id profile-id}] - (->> (perms/assign-role-flags params role) - (db/insert! conn :team-profile-rel)))) - -(defn- create-team-default-project - [conn {:keys [team-id profile-id] :as params}] - (let [project {:id (uuid/next) - :team-id team-id - :name "Drafts" - :is-default true} - project (projects/create-project conn project)] - (projects/create-project-role conn {:project-id (:id project) - :profile-id profile-id - :role :owner}) - project)) + (cmd.teams/create-team conn params))) ;; --- Mutation: Update Team -(s/def ::update-team - (s/keys :req-un [::profile-id ::name ::id])) +(s/def ::update-team ::cmd.teams/update-team) (sv/defmethod ::update-team - [{:keys [pool] :as cfg} {:keys [id name profile-id] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [id name profile-id] :as params}] (db/with-atomic [conn pool] - (teams/check-edition-permissions! conn profile-id id) + (cmd.teams/check-edition-permissions! conn profile-id id) (db/update! conn :team {:name name} {:id id}) nil)) - ;; --- Mutation: Leave Team -(declare role->params) - -(s/def ::reassign-to ::us/uuid) -(s/def ::leave-team - (s/keys :req-un [::profile-id ::id] - :opt-un [::reassign-to])) +(s/def ::leave-team ::cmd.teams/leave-team) (sv/defmethod ::leave-team - [{:keys [pool] :as cfg} {:keys [id profile-id reassign-to]}] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] - (let [perms (teams/get-permissions conn profile-id id) - members (teams/retrieve-team-members conn id)] - - (cond - ;; we can only proceed if there are more members in the team - ;; besides the current profile - (<= (count members) 1) - (ex/raise :type :validation - :code :no-enough-members-for-leave - :context {:members (count members)}) - - ;; if the `reassign-to` is filled and has a different value - ;; than the current profile-id, we proceed to reassing the - ;; owner role to profile identified by the `reassign-to`. - (and reassign-to (not= reassign-to profile-id)) - (let [member (d/seek #(= reassign-to (:id %)) members)] - (when-not member - (ex/raise :type :not-found :code :member-does-not-exist)) - - ;; unasign owner role to current profile - (db/update! conn :team-profile-rel - {:is-owner false} - {:team-id id - :profile-id profile-id}) - - ;; assign owner role to new profile - (db/update! conn :team-profile-rel - (role->params :owner) - {:team-id id :profile-id reassign-to})) - - ;; and finally, if all other conditions does not match and the - ;; current profile is owner, we dont allow it because there - ;; must always be an owner. - (:is-owner perms) - (ex/raise :type :validation - :code :owner-cant-leave-team - :hint "releasing owner before leave")) - - (db/delete! conn :team-profile-rel - {:profile-id profile-id - :team-id id}) - - nil))) + (cmd.teams/leave-team conn params))) ;; --- Mutation: Delete Team -(s/def ::delete-team - (s/keys :req-un [::profile-id ::id])) - -;; TODO: right now just don't allow delete default team, in future it -;; should raise a specific exception for signal that this action is -;; not allowed. +(s/def ::delete-team ::cmd.teams/delete-team) (sv/defmethod ::delete-team - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [id profile-id] :as params}] (db/with-atomic [conn pool] - (let [perms (teams/get-permissions conn profile-id id)] + (let [perms (cmd.teams/get-permissions conn profile-id id)] (when-not (:is-owner perms) (ex/raise :type :validation :code :only-owner-can-delete-team)) - (db/update! conn :team {:deleted-at (dt/now)} {:id id :is-default false}) @@ -189,89 +84,29 @@ ;; --- Mutation: Team Update Role -(declare retrieve-team-member) - -(s/def ::team-id ::us/uuid) -(s/def ::member-id ::us/uuid) -;; Temporarily disabled viewer role -;; https://tree.taiga.io/project/uxboxproject/issue/1083 -;; (s/def ::role #{:owner :admin :editor :viewer}) -(s/def ::role #{:owner :admin :editor}) - -(s/def ::update-team-member-role - (s/keys :req-un [::profile-id ::team-id ::member-id ::role])) +(s/def ::update-team-member-role ::cmd.teams/update-team-member-role) (sv/defmethod ::update-team-member-role - [{:keys [pool] :as cfg} {:keys [team-id profile-id member-id role] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] - (let [perms (teams/get-permissions conn profile-id team-id) - ;; We retrieve all team members instead of query the - ;; database for a single member. This is just for - ;; convenience, if this becomes a bottleneck or problematic, - ;; we will change it to more efficient fetch mechanisms. - members (teams/retrieve-team-members conn team-id) - member (d/seek #(= member-id (:id %)) members) - - is-owner? (:is-owner perms) - is-admin? (:is-admin perms)] - - ;; If no member is found, just 404 - (when-not member - (ex/raise :type :not-found - :code :member-does-not-exist)) - - ;; First check if we have permissions to change roles - (when-not (or is-owner? is-admin?) - (ex/raise :type :validation - :code :insufficient-permissions)) - - ;; Don't allow change role of owner member - (when (:is-owner member) - (ex/raise :type :validation - :code :cant-change-role-to-owner)) - - ;; Don't allow promote to owner to admin users. - (when (and (not is-owner?) (= role :owner)) - (ex/raise :type :validation - :code :cant-promote-to-owner)) - - (let [params (role->params role)] - ;; Only allow single owner on team - (when (= role :owner) - (db/update! conn :team-profile-rel - {:is-owner false} - {:team-id team-id - :profile-id profile-id})) - - (db/update! conn :team-profile-rel - params - {:team-id team-id - :profile-id member-id}) - nil)))) - -(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})) - + (cmd.teams/update-team-member-role conn params))) ;; --- Mutation: Delete Team Member -(s/def ::delete-team-member - (s/keys :req-un [::profile-id ::team-id ::member-id])) +(s/def ::delete-team-member ::cmd.teams/delete-team-member) (sv/defmethod ::delete-team-member - [{:keys [pool] :as cfg} {:keys [team-id profile-id member-id] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [team-id profile-id member-id] :as params}] (db/with-atomic [conn pool] - (let [perms (teams/get-permissions conn profile-id team-id)] + (let [perms (cmd.teams/get-permissions conn profile-id team-id)] (when-not (or (:is-owner perms) (:is-admin perms)) (ex/raise :type :validation :code :insufficient-permissions)) - (when (= member-id profile-id) (ex/raise :type :validation :code :cant-remove-yourself)) @@ -283,85 +118,27 @@ ;; --- Mutation: Update Team Photo -(declare ^:private upload-photo) -(declare ^:private update-team-photo) - -(s/def ::file ::media/upload) -(s/def ::update-team-photo - (s/keys :req-un [::profile-id ::team-id ::file])) +(s/def ::update-team-photo ::cmd.teams/update-team-photo) (sv/defmethod ::update-team-photo + {::doc/added "1.0" + ::doc/deprecated "1.17"} [cfg {:keys [file] :as params}] ;; Validate incoming mime type (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) (let [cfg (update cfg :storage media/configure-assets-storage)] - (update-team-photo cfg params))) - -(defn update-team-photo - [{:keys [pool storage executor] :as cfg} {:keys [profile-id team-id] :as params}] - (p/let [team (px/with-dispatch executor - (teams/retrieve-team pool profile-id team-id)) - photo (upload-photo cfg params)] - - ;; Mark object as touched for make it ellegible for tentative - ;; garbage collection. - (when-let [id (:photo-id team)] - (sto/touch-object! storage id)) - - ;; Save new photo - (db/update! pool :team - {:photo-id (:id photo)} - {:id team-id}) - - (assoc team :photo-id (:id photo)))) - -(defn upload-photo - [{:keys [storage executor climit] :as cfg} {:keys [file]}] - (letfn [(get-info [content] - (climit/with-dispatch (:process-image climit) - (media/run {:cmd :info :input content}))) - - (generate-thumbnail [info] - (climit/with-dispatch (:process-image climit) - (media/run {:cmd :profile-thumbnail - :format :jpeg - :quality 85 - :width 256 - :height 256 - :input info}))) - - ;; Function responsible of calculating cryptographyc hash of - ;; the provided data. - (calculate-hash [data] - (px/with-dispatch executor - (sto/calculate-hash data)))] - - (p/let [info (get-info file) - thumb (generate-thumbnail info) - hash (calculate-hash (:data thumb)) - content (-> (sto/content (:data thumb) (:size thumb)) - (sto/wrap-with-hash hash))] - (sto/put-object! storage {::sto/content content - ::sto/deduplicate? true - :bucket "profile" - :content-type (:mtype thumb)})))) + (cmd.teams/update-team-photo cfg params))) ;; --- Mutation: Invite Member -(declare create-team-invitation) - -(s/def ::email ::us/email) -(s/def ::emails ::us/set-of-valid-emails) -(s/def ::invite-team-member - (s/keys :req-un [::profile-id ::team-id ::role] - :opt-un [::email ::emails])) +(s/def ::invite-team-member ::cmd.teams/create-team-invitations) (sv/defmethod ::invite-team-member - "A rpc call that allow to send a single or multiple invitations to - join the team." - [{:keys [pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}] (db/with-atomic [conn pool] - (let [perms (teams/get-permissions conn profile-id team-id) + (let [perms (cmd.teams/get-permissions conn profile-id team-id) profile (db/get-by-id conn :profile profile-id) team (db/get-by-id conn :team team-id) emails (cond-> (or emails #{}) (string? email) (conj email))] @@ -384,101 +161,25 @@ :team team :profile profile :role role))) - (map create-team-invitation))] + (map #'cmd.teams/create-invitation))] (with-meta (vec invitations) {::audit/props {:invitations (count invitations)}}))))) -(def sql:upsert-team-invitation - "insert into team_invitation(team_id, email_to, role, valid_until) - values (?, ?, ?, ?) - on conflict(team_id, email_to) do - update set role = ?, valid_until = ?, updated_at = now();") - -(defn- create-team-invitation - [{:keys [conn sprops team profile role email] :as cfg}] - (let [member (profile/retrieve-profile-data-by-email conn email) - token-exp (dt/in-future "168h") ;; 7 days - email (str/lower email) - itoken (tokens/generate sprops - {:iss :team-invitation - :exp token-exp - :profile-id (:id profile) - :role role - :team-id (:id team) - :member-email (:email member email) - :member-id (:id member)}) - ptoken (tokens/generate sprops - {:iss :profile-identity - :profile-id (:id profile) - :exp (dt/in-future {:days 30})})] - - (when (and member (not (eml/allow-send-emails? conn member))) - (ex/raise :type :validation - :code :member-is-muted - :email email - :hint "the profile has reported repeatedly as spam or has bounces")) - - ;; Secondly check if the invited member email is part of the global spam/bounce report. - (when (eml/has-bounce-reports? conn email) - (ex/raise :type :validation - :code :email-has-permanent-bounces - :email email - :hint "the email you invite has been repeatedly reported as spam or bounce")) - - (when (contains? cf/flags :log-invitation-tokens) - (l/trace :hint "invitation token" :token itoken)) - - ;; 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 {: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)}))) - (do - (db/exec-one! conn [sql:upsert-team-invitation - (:id team) (str/lower email) (name role) - token-exp (name role) token-exp]) - (eml/send! {::eml/conn conn - ::eml/factory eml/invite-to-team - :public-uri (:public-uri cfg) - :to email - :invited-by (:fullname profile) - :team (:name team) - :token itoken - :extra-data ptoken}))) - - itoken)) - ;; --- Mutation: Create Team & Invite Members -(s/def ::emails ::us/set-of-valid-emails) -(s/def ::create-team-and-invite-members - (s/and ::create-team (s/keys :req-un [::emails ::role]))) +(s/def ::create-team-and-invite-members ::cmd.teams/create-team-and-invitations) (sv/defmethod ::create-team-and-invite-members - [{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id emails role] :as params}] (db/with-atomic [conn pool] - (let [team (create-team conn params) + (let [team (cmd.teams/create-team conn params) profile (db/get-by-id conn :profile profile-id)] ;; Create invitations for all provided emails. (doseq [email emails] - (create-team-invitation + (#'cmd.teams/create-invitation (assoc cfg :conn conn :team team @@ -505,9 +206,11 @@ (s/keys :req-un [::profile-id ::team-id ::email ::role])) (sv/defmethod ::update-team-invitation-role - [{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email role] :as params}] (db/with-atomic [conn pool] - (let [perms (teams/get-permissions conn profile-id team-id)] + (let [perms (cmd.teams/get-permissions conn profile-id team-id)] (when-not (:is-admin perms) (ex/raise :type :validation @@ -520,13 +223,14 @@ ;; --- Mutation: Delete invitation -(s/def ::delete-team-invitation - (s/keys :req-un [::profile-id ::team-id ::email])) +(s/def ::delete-team-invitation ::cmd.teams/delete-team-invitation) (sv/defmethod ::delete-team-invitation - [{:keys [pool] :as cfg} {:keys [profile-id team-id email] :as params}] + {::doc/added "1.0" + ::doc/deprecated "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email] :as params}] (db/with-atomic [conn pool] - (let [perms (teams/get-permissions conn profile-id team-id)] + (let [perms (cmd.teams/get-permissions conn profile-id team-id)] (when-not (:is-admin perms) (ex/raise :type :validation diff --git a/backend/src/app/rpc/queries/comments.clj b/backend/src/app/rpc/queries/comments.clj index e9db1a6c8..fbcb86f03 100644 --- a/backend/src/app/rpc/queries/comments.clj +++ b/backend/src/app/rpc/queries/comments.clj @@ -9,8 +9,8 @@ [app.db :as db] [app.rpc.commands.comments :as cmd.comments] [app.rpc.commands.files :as cmd.files] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] - [app.rpc.queries.teams :as teams] [app.util.services :as sv] [clojure.spec.alpha :as s])) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 0672e5f89..398b1400a 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -8,38 +8,38 @@ (:require [app.common.spec :as us] [app.db :as db] - [app.rpc.commands.files :as cmd.files] - [app.rpc.commands.search :as cmd.search] + [app.rpc.commands.files :as files] + [app.rpc.commands.search :as search] + [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.queries.projects :as projects] - [app.rpc.queries.teams :as teams] [app.util.services :as sv] [clojure.spec.alpha :as s])) ;; --- Query: Project Files -(s/def ::project-files ::cmd.files/get-project-files) +(s/def ::project-files ::files/get-project-files) (sv/defmethod ::project-files - {::doc/added "1.1" + {::doc/added "1.0" ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] (with-open [conn (db/open pool)] (projects/check-read-permissions! conn profile-id project-id) - (cmd.files/get-project-files conn project-id))) + (files/get-project-files conn project-id))) ;; --- Query: File (By ID) (s/def ::components-v2 ::us/boolean) (s/def ::file - (s/and ::cmd.files/get-file + (s/and ::files/get-file (s/keys :opt-un [::components-v2]))) (defn get-file [conn id features] - (let [file (cmd.files/get-file conn id features) - thumbs (cmd.files/get-object-thumbnails conn id)] + (let [file (files/get-file conn id features) + thumbs (files/get-object-thumbnails conn id)] (assoc file :thumbnails thumbs))) (sv/defmethod ::file @@ -48,19 +48,19 @@ ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id id features components-v2] :as params}] (with-open [conn (db/open pool)] - (let [perms (cmd.files/get-permissions pool profile-id id) + (let [perms (files/get-permissions pool profile-id id) ;; BACKWARD COMPATIBILTY with the components-v2 parameter features (cond-> (or features #{}) components-v2 (conj "components/v2"))] - (cmd.files/check-read-permissions! perms) + (files/check-read-permissions! perms) (-> (get-file conn id features) (assoc :permissions perms))))) ;; --- QUERY: page (s/def ::page - (s/and ::cmd.files/get-page + (s/and ::files/get-page (s/keys :opt-un [::components-v2]))) (sv/defmethod ::page @@ -77,18 +77,18 @@ ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as params}] (with-open [conn (db/open pool)] - (cmd.files/check-read-permissions! conn profile-id file-id) + (files/check-read-permissions! conn profile-id file-id) (let [;; BACKWARD COMPATIBILTY with the components-v2 parameter features (cond-> (or features #{}) components-v2 (conj "components/v2")) params (assoc params :features features)] - (cmd.files/get-page conn params)))) + (files/get-page conn params)))) ;; --- QUERY: file-data-for-thumbnail (s/def ::file-data-for-thumbnail - (s/and ::cmd.files/get-file-data-for-thumbnail + (s/and ::files/get-file-data-for-thumbnail (s/keys :opt-un [::components-v2]))) (sv/defmethod ::file-data-for-thumbnail @@ -98,18 +98,18 @@ ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as props}] (with-open [conn (db/open pool)] - (cmd.files/check-read-permissions! conn profile-id file-id) + (files/check-read-permissions! conn profile-id file-id) (let [;; BACKWARD COMPATIBILTY with the components-v2 parameter features (cond-> (or features #{}) components-v2 (conj "components/v2")) - file (cmd.files/get-file conn file-id features)] + file (files/get-file conn file-id features)] {:file-id file-id :revn (:revn file) - :page (cmd.files/get-file-data-for-thumbnail conn file)}))) + :page (files/get-file-data-for-thumbnail conn file)}))) ;; --- Query: Shared Library Files -(s/def ::team-shared-files ::cmd.files/get-team-shared-files) +(s/def ::team-shared-files ::files/get-team-shared-files) (sv/defmethod ::team-shared-files {::doc/added "1.3" @@ -117,37 +117,37 @@ [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) - (cmd.files/get-team-shared-files conn params))) + (files/get-team-shared-files conn params))) ;; --- Query: File Libraries used by a File -(s/def ::file-libraries ::cmd.files/get-file-libraries) +(s/def ::file-libraries ::files/get-file-libraries) (sv/defmethod ::file-libraries {::doc/added "1.3" ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id file-id features] :as params}] (with-open [conn (db/open pool)] - (cmd.files/check-read-permissions! conn profile-id file-id) - (cmd.files/get-file-libraries conn file-id features))) + (files/check-read-permissions! conn profile-id file-id) + (files/get-file-libraries conn file-id features))) ;; --- Query: Files that use this File library -(s/def ::library-using-files ::cmd.files/get-library-file-references) +(s/def ::library-using-files ::files/get-library-file-references) (sv/defmethod ::library-using-files {::doc/added "1.13" ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (with-open [conn (db/open pool)] - (cmd.files/check-read-permissions! conn profile-id file-id) - (cmd.files/get-library-file-references conn file-id))) + (files/check-read-permissions! conn profile-id file-id) + (files/get-library-file-references conn file-id))) ;; --- QUERY: team-recent-files -(s/def ::team-recent-files ::cmd.files/get-team-recent-files) +(s/def ::team-recent-files ::files/get-team-recent-files) (sv/defmethod ::team-recent-files {::doc/added "1.0" @@ -155,30 +155,30 @@ [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) - (cmd.files/get-team-recent-files conn team-id))) + (files/get-team-recent-files conn team-id))) ;; --- QUERY: get file thumbnail -(s/def ::file-thumbnail ::cmd.files/get-file-thumbnail) +(s/def ::file-thumbnail ::files/get-file-thumbnail) (sv/defmethod ::file-thumbnail {::doc/added "1.13" ::doc/deprecated "1.17"} [{:keys [pool]} {:keys [profile-id file-id revn]}] (with-open [conn (db/open pool)] - (cmd.files/check-read-permissions! conn profile-id file-id) - (-> (cmd.files/get-file-thumbnail conn file-id revn) - (rph/with-http-cache cmd.files/long-cache-duration)))) + (files/check-read-permissions! conn profile-id file-id) + (-> (files/get-file-thumbnail conn file-id revn) + (rph/with-http-cache files/long-cache-duration)))) ;; --- QUERY: search files -(s/def ::search-files ::cmd.search/search-files) +(s/def ::search-files ::search/search-files) (sv/defmethod ::search-files {::doc/added "1.0" ::doc/deprecated "1.17"} [{:keys [pool]} {:keys [search-term] :as params}] (when search-term - (cmd.search/search-files pool params))) + (search/search-files pool params))) diff --git a/backend/src/app/rpc/queries/fonts.clj b/backend/src/app/rpc/queries/fonts.clj index 077766cd3..ac592ed1a 100644 --- a/backend/src/app/rpc/queries/fonts.clj +++ b/backend/src/app/rpc/queries/fonts.clj @@ -9,13 +9,14 @@ [app.common.spec :as us] [app.db :as db] [app.rpc.commands.files :as files] + [app.rpc.commands.teams :as teams] [app.rpc.queries.projects :as projects] - [app.rpc.queries.teams :as teams] [app.util.services :as sv] [clojure.spec.alpha :as s])) ;; --- Query: Team Font Variants +;; FIXME: PLEASE RIGHT NOW ;; TODO: deprecated, should be removed on 1.7.x (s/def ::team-id ::us/uuid) diff --git a/backend/src/app/rpc/queries/projects.clj b/backend/src/app/rpc/queries/projects.clj index 2df15daa1..64c4a9b42 100644 --- a/backend/src/app/rpc/queries/projects.clj +++ b/backend/src/app/rpc/queries/projects.clj @@ -8,8 +8,8 @@ (:require [app.common.spec :as us] [app.db :as db] + [app.rpc.commands.teams :as teams] [app.rpc.permissions :as perms] - [app.rpc.queries.teams :as teams] [app.util.services :as sv] [clojure.spec.alpha :as s])) diff --git a/backend/src/app/rpc/queries/teams.clj b/backend/src/app/rpc/queries/teams.clj index abcb543a6..10801413e 100644 --- a/backend/src/app/rpc/queries/teams.clj +++ b/backend/src/app/rpc/queries/teams.clj @@ -6,244 +6,82 @@ (ns app.rpc.queries.teams (:require - [app.common.exceptions :as ex] - [app.common.spec :as us] [app.db :as db] - [app.rpc.permissions :as perms] - [app.rpc.queries.profile :as profile] + [app.rpc.commands.teams :as cmd.teams] + [app.rpc.doc :as-alias doc] [app.util.services :as sv] [clojure.spec.alpha :as s])) -;; --- Team Edition Permissions - -(def ^:private sql:team-permissions - "select tpr.is_owner, - tpr.is_admin, - tpr.can_edit - from team_profile_rel as tpr - join team as t on (t.id = tpr.team_id) - where tpr.profile_id = ? - and tpr.team_id = ? - and t.deleted_at is null") - -(defn get-permissions - [conn profile-id team-id] - (let [rows (db/exec! conn [sql:team-permissions profile-id team-id]) - is-owner (boolean (some :is-owner rows)) - is-admin (boolean (some :is-admin rows)) - can-edit (boolean (some :can-edit rows))] - (when (seq rows) - {:is-owner is-owner - :is-admin (or is-owner is-admin) - :can-edit (or is-owner is-admin can-edit) - :can-read true}))) - -(def has-edit-permissions? - (perms/make-edition-predicate-fn get-permissions)) - -(def has-read-permissions? - (perms/make-read-predicate-fn get-permissions)) - -(def check-edition-permissions! - (perms/make-check-fn has-edit-permissions?)) - -(def check-read-permissions! - (perms/make-check-fn has-read-permissions?)) - ;; --- Query: Teams -(declare retrieve-teams) - -(s/def ::profile-id ::us/uuid) -(s/def ::teams - (s/keys :req-un [::profile-id])) +(s/def ::teams ::cmd.teams/get-teams) (sv/defmethod ::teams + {::doc/added "1.0" + ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id]}] (with-open [conn (db/open pool)] - (retrieve-teams conn profile-id))) - -(def sql:teams - "select t.*, - tp.is_owner, - tp.is_admin, - tp.can_edit, - (t.id = ?) as is_default - from team_profile_rel as tp - join team as t on (t.id = tp.team_id) - where t.deleted_at is null - and tp.profile_id = ? - order by tp.created_at asc") - -(defn process-permissions - [team] - (let [is-owner (:is-owner team) - is-admin (:is-admin team) - can-edit (:can-edit team) - permissions {:type :membership - :is-owner is-owner - :is-admin (or is-owner is-admin) - :can-edit (or is-owner is-admin can-edit)}] - (-> team - (dissoc :is-owner :is-admin :can-edit) - (assoc :permissions permissions)))) - -(defn retrieve-teams - [conn profile-id] - (let [defaults (profile/retrieve-additional-data conn profile-id)] - (->> (db/exec! conn [sql:teams (:default-team-id defaults) profile-id]) - (mapv process-permissions)))) + (cmd.teams/retrieve-teams conn profile-id))) ;; --- Query: Team (by ID) -(declare retrieve-team) - -(s/def ::id ::us/uuid) -(s/def ::team - (s/keys :req-un [::profile-id ::id])) +(s/def ::team ::cmd.teams/get-team) (sv/defmethod ::team + {::doc/added "1.0" + ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id id]}] (with-open [conn (db/open pool)] - (retrieve-team conn profile-id id))) - -(defn retrieve-team - [conn profile-id team-id] - (let [defaults (profile/retrieve-additional-data conn profile-id) - sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?") - result (db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id])] - (when-not result - (ex/raise :type :not-found - :code :team-does-not-exist)) - (process-permissions result))) - + (cmd.teams/retrieve-team conn profile-id id))) ;; --- Query: Team Members -(declare retrieve-team-members) - -(s/def ::team-id ::us/uuid) -(s/def ::team-members - (s/keys :req-un [::profile-id ::team-id])) +(s/def ::team-members ::cmd.teams/get-team-members) (sv/defmethod ::team-members + {::doc/added "1.0" + ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] (with-open [conn (db/open pool)] - (check-read-permissions! conn profile-id team-id) - (retrieve-team-members conn team-id))) - -(def sql:team-members - "select tp.*, - p.id, - p.email, - p.fullname as name, - p.fullname as fullname, - p.photo_id, - p.is_active - from team_profile_rel as tp - join profile as p on (p.id = tp.profile_id) - where tp.team_id = ?") - -(defn retrieve-team-members - [conn team-id] - (db/exec! conn [sql:team-members team-id])) - + (cmd.teams/check-read-permissions! conn profile-id team-id) + (cmd.teams/retrieve-team-members conn team-id))) ;; --- Query: Team Users - -(declare retrieve-users) -(declare retrieve-team-for-file) - -(s/def ::file-id ::us/uuid) -(s/def ::team-users - (s/and (s/keys :req-un [::profile-id] - :opt-un [::team-id ::file-id]) - #(or (:team-id %) (:file-id %)))) +(s/def ::team-users ::cmd.teams/get-team-users) (sv/defmethod ::team-users + {::doc/added "1.0" + ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id team-id file-id]}] (with-open [conn (db/open pool)] (if team-id (do - (check-read-permissions! conn profile-id team-id) - (retrieve-users conn team-id)) - (let [{team-id :id} (retrieve-team-for-file conn file-id)] - (check-read-permissions! conn profile-id team-id) - (retrieve-users conn team-id))))) - -;; This is a similar query to team members but can contain more data -;; because some user can be explicitly added to project or file (not -;; implemented in UI) - -(def sql:team-users - "select pf.id, pf.fullname, pf.photo_id - from profile as pf - inner join team_profile_rel as tpr on (tpr.profile_id = pf.id) - where tpr.team_id = ? - union - select pf.id, pf.fullname, pf.photo_id - from profile as pf - inner join project_profile_rel as ppr on (ppr.profile_id = pf.id) - inner join project as p on (ppr.project_id = p.id) - where p.team_id = ? - union - select pf.id, pf.fullname, pf.photo_id - from profile as pf - inner join file_profile_rel as fpr on (fpr.profile_id = pf.id) - inner join file as f on (fpr.file_id = f.id) - inner join project as p on (f.project_id = p.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 retrieve-users - [conn team-id] - (db/exec! conn [sql:team-users team-id team-id team-id])) - -(defn retrieve-team-for-file - [conn file-id] - (->> [sql:team-by-file file-id] - (db/exec-one! conn))) + (cmd.teams/check-read-permissions! conn profile-id team-id) + (cmd.teams/retrieve-users conn team-id)) + (let [{team-id :id} (cmd.teams/retrieve-team-for-file conn file-id)] + (cmd.teams/check-read-permissions! conn profile-id team-id) + (cmd.teams/retrieve-users conn team-id))))) ;; --- Query: Team Stats -(declare retrieve-team-stats) - -(s/def ::team-stats - (s/keys :req-un [::profile-id ::team-id])) +(s/def ::team-stats ::cmd.teams/get-team-stats) (sv/defmethod ::team-stats + {::doc/added "1.0" + ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] (with-open [conn (db/open pool)] - (check-read-permissions! conn profile-id team-id) - (retrieve-team-stats conn team-id))) - -(def sql:team-stats - "select (select count(*) from project where team_id = ?) as projects, - (select count(*) from file as f join project as p on (p.id = f.project_id) where p.team_id = ?) as files") - -(defn retrieve-team-stats - [conn team-id] - (db/exec-one! conn [sql:team-stats team-id team-id])) - + (cmd.teams/check-read-permissions! conn profile-id team-id) + (cmd.teams/retrieve-team-stats conn team-id))) ;; --- Query: Team invitations -(s/def ::team-id ::us/uuid) -(s/def ::team-invitations - (s/keys :req-un [::profile-id ::team-id])) - -(def sql:team-invitations - "select email_to as email, role, (valid_until < now()) as expired - from team_invitation where team_id = ? order by valid_until desc") +(s/def ::team-invitations ::cmd.teams/get-team-invitations) (sv/defmethod ::team-invitations + {::doc/added "1.0" + ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] (with-open [conn (db/open pool)] - (check-read-permissions! conn profile-id team-id) - (->> (db/exec! conn [sql:team-invitations team-id]) - (mapv #(update % :role keyword))))) + (cmd.teams/check-read-permissions! conn profile-id team-id) + (cmd.teams/get-team-invitations conn team-id))) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 41cf3e1cf..7b2792ca4 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -17,14 +17,13 @@ [app.main :as main] [app.media] [app.migrations] - [app.rpc.helpers :as rph] [app.rpc.commands.auth :as cmd.auth] [app.rpc.commands.files :as files] [app.rpc.commands.files.create :as files.create] [app.rpc.commands.files.update :as files.update] + [app.rpc.commands.teams :as teams] + [app.rpc.helpers :as rph] [app.rpc.mutations.profile :as profile] - [app.rpc.mutations.projects :as projects] - [app.rpc.mutations.teams :as teams] [app.util.blob :as blob] [app.util.services :as sv] [app.util.time :as dt] @@ -172,7 +171,7 @@ (->> (merge {:id (mk-uuid "project" i) :name (str "project" i)} params) - (#'projects/create-project conn))))) + (#'teams/create-project conn))))) (defn create-file* ([i params] @@ -254,7 +253,7 @@ ([params] (create-project-role* *pool* params)) ([pool {:keys [project-id profile-id role] :or {role :owner}}] (with-open [conn (db/open pool)] - (#'projects/create-project-role conn {:project-id project-id + (#'teams/create-project-role conn {:project-id project-id :profile-id profile-id :role role})))) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 16feb7014..ca05fbfa2 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -110,7 +110,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/query! :team-members {:team-id team-id}) + (->> (rp/cmd! :get-team-members {:team-id team-id}) (rx/map team-members-fetched)))))) ;; --- EVENT: fetch-team-stats @@ -128,7 +128,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/query! :team-stats {:team-id team-id}) + (->> (rp/cmd! :get-team-stats {:team-id team-id}) (rx/map team-stats-fetched)))))) ;; --- EVENT: fetch-team-invitations @@ -146,7 +146,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/query! :team-invitations {:team-id team-id}) + (->> (rp/cmd! :get-team-invitations {:team-id team-id}) (rx/map team-invitations-fetched)))))) ;; --- EVENT: fetch-team-webhooks @@ -384,14 +384,13 @@ (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] - (->> (rp/mutation! :create-team {:name name}) + (->> (rp/cmd! :create-team {:name name}) (rx/tap on-success) (rx/map team-created) (rx/catch on-error)))))) ;; --- EVENT: create-team-with-invitations - (defn create-team-with-invitations [{:keys [name emails role] :as params}] (us/assert! ::us/string name) @@ -404,7 +403,7 @@ params {:name name :emails #{emails} :role role}] - (->> (rp/mutation! :create-team-and-invite-members params) + (->> (rp/cmd! :create-team-and-invitations params) (rx/tap on-success) (rx/map team-created) (rx/catch on-error)))))) @@ -421,7 +420,7 @@ ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation! :update-team params) + (->> (rp/cmd! :update-team params) (rx/ignore))))) (defn update-team-photo @@ -440,7 +439,7 @@ (->> (rx/of file) (rx/map di/validate-file) (rx/map prepare) - (rx/mapcat #(rp/mutation :update-team-photo %)) + (rx/mapcat #(rp/cmd! :update-team-photo %)) (rx/do on-success) (rx/map du/fetch-teams) (rx/catch on-error)))))) @@ -454,7 +453,7 @@ (watch [_ state _] (let [team-id (:current-team-id state) params (assoc params :team-id team-id)] - (->> (rp/mutation! :update-team-member-role params) + (->> (rp/cmd! :update-team-member-role params) (rx/mapcat (fn [_] (rx/of (fetch-team-members) (du/fetch-teams))))))))) @@ -467,7 +466,7 @@ (watch [_ state _] (let [team-id (:current-team-id state) params (assoc params :team-id team-id)] - (->> (rp/mutation! :delete-team-member params) + (->> (rp/cmd! :delete-team-member params) (rx/mapcat (fn [_] (rx/of (fetch-team-members) (du/fetch-teams))))))))) @@ -487,7 +486,7 @@ params (cond-> {:id team-id} (uuid? reassign-to) (assoc :reassign-to reassign-to))] - (->> (rp/mutation! :leave-team params) + (->> (rp/cmd! :leave-team params) (rx/tap #(tm/schedule on-success)) (rx/catch on-error)))))) @@ -506,7 +505,7 @@ :or {on-success identity on-error rx/throw}} (meta params) params (dissoc params :resend?)] - (->> (rp/mutation! :invite-team-member params) + (->> (rp/cmd! :create-team-invitations params) (rx/tap on-success) (rx/catch on-error)))))) @@ -524,7 +523,7 @@ (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] - (->> (rp/mutation! :update-team-invitation-role params) + (->> (rp/cmd! :update-team-invitation-role params) (rx/tap on-success) (rx/catch on-error)))))) @@ -538,7 +537,7 @@ (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] - (->> (rp/mutation! :delete-team-invitation params) + (->> (rp/cmd! :delete-team-invitation params) (rx/tap on-success) (rx/catch on-error)))))) @@ -608,7 +607,7 @@ (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] - (->> (rp/mutation! :delete-team {:id id}) + (->> (rp/cmd! :delete-team {:id id}) (rx/tap on-success) (rx/catch on-error)))))) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 51f962622..906ddfd7a 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -87,7 +87,7 @@ (ptk/reify ::fetch-teams ptk/WatchEvent (watch [_ _ _] - (->> (rp/query! :teams) + (->> (rp/cmd! :get-teams) (rx/map teams-fetched))))) ;; --- EVENT: fetch-profile @@ -446,7 +446,7 @@ (ptk/reify ::fetch-team-users ptk/WatchEvent (watch [_ _ _] - (->> (rp/query! :team-users {:team-id team-id}) + (->> (rp/cmd! :get-team-users {:team-id team-id}) (rx/map #(partial fetched %))))))) (defn fetch-file-comments-users diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 74d1e1c1a..e6517f329 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -265,7 +265,7 @@ (->> (rx/zip (rp/cmd! :get-file {:id file-id :features features}) (rp/cmd! :get-file-object-thumbnails {:file-id file-id}) (rp/query! :project {:id project-id}) - (rp/query! :team-users {:file-id file-id}) + (rp/cmd! :get-team-users {:file-id file-id}) (rp/cmd! :get-profiles-for-file-comments {:file-id file-id :share-id share-id})) (rx/take 1) (rx/map (partial bundle-fetched features)) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index aea3ed169..2fa651daf 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -17,6 +17,11 @@ (derive :get-file-libraries ::query) (derive :get-file-fragment ::query) (derive :search-files ::query) +(derive :get-teams ::query) +(derive :get-team-users ::query) +(derive :get-team-members ::query) +(derive :get-team-stats ::query) +(derive :get-team-invitations ::query) (defn handle-response [{:keys [status body] :as response}]