From 84903ae1f252d44fca5b8b9683b49d1b5266b3ac Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 13 Dec 2022 22:13:49 +0100 Subject: [PATCH 01/31] :bug: Fix unable to select text at assets inputs in firefox --- CHANGES.md | 1 + .../src/app/main/ui/workspace/sidebar/assets.cljs | 11 +++++++---- .../workspace/sidebar/options/menus/typography.cljs | 5 ++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 22a8037b5..dfa47c58a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,7 @@ - Fix twitter support account link [Taiga #4279](https://tree.taiga.io/project/penpot/issue/4279) - Fix lang autodetect issue [Taiga #4277](https://tree.taiga.io/project/penpot/issue/4277) - Fix adding an extra page on import [Taiga #4543](https://tree.taiga.io/project/penpot/task/4543) +- Fix unable to select text at assets inputs in firefox [Taiga #4572](https://tree.taiga.io/project/penpot/issue/4572) ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 9746e0438..2f5434f33 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -1286,7 +1286,7 @@ #(on-asset-click % (:id color) (partial apply-color (:id color)))) :ref item-ref - :draggable (not workspace-read-only?) + :draggable (and (not workspace-read-only?) (not (:editing @state))) :on-drag-start on-color-drag-start :on-drag-enter on-drag-enter :on-drag-leave on-drag-leave @@ -1557,6 +1557,8 @@ (let [item-ref (mf/use-ref) dragging? (mf/use-state false) workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) + editing? (= editing-id (:id typography)) + open? (mf/use-state editing?) on-drop (mf/use-fn (mf/deps typography dragging? selected-typographies selected-typographies-full selected-typographies-paths move-typography) @@ -1587,7 +1589,7 @@ (on-asset-drag-start event typography selected-typographies item-ref :typographies identity))))] [:div.typography-container {:ref item-ref - :draggable (not workspace-read-only?) + :draggable (and (not workspace-read-only?) (not @open?)) :on-drag-start on-typography-drag-start :on-drag-enter on-drag-enter :on-drag-leave on-drag-leave @@ -1603,8 +1605,9 @@ :selected? (contains? selected-typographies (:id typography)) :on-click #(on-asset-click % (:id typography) (partial apply-typography typography)) - :editing? (= editing-id (:id typography)) - :focus-name? (= (:rename-typography local-data) (:id typography))}] + :editing? editing? + :focus-name? (= (:rename-typography local-data) (:id typography)) + :open? open?}] (when @dragging? [:div.dragging])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 62c77cb0d..a6fae4c3b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -456,9 +456,8 @@ ;; In summary, this need to a good UX/UI/IMPL rework. (mf/defc typography-entry - [{:keys [typography local? selected? on-click on-change on-detach on-context-menu editing? focus-name? file]}] - (let [open? (mf/use-state editing?) - hover-detach (mf/use-state false) + [{:keys [typography local? selected? on-click on-change on-detach on-context-menu editing? focus-name? file open?]}] + (let [hover-detach (mf/use-state false) name-input-ref (mf/use-ref) on-change-ref (mf/use-ref nil) workspace-read-only? (mf/use-ctx ctx/workspace-read-only?) From e88d6d88a86fdb01a4eb47e22df1af5a1b891dd8 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 15 Dec 2022 10:10:04 +0100 Subject: [PATCH 02/31] :bug: Fix strage cursor behaviour after clicking viewport with text pool --- CHANGES.md | 1 + common/src/app/common/geom/shapes/text.cljc | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1eef88e81..e63cb260c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # CHANGELOG ## 1.16.2-beta +- Fix strage cursor behaviour after clicking viewport with text pool [Github #2447](https://github.com/penpot/penpot/issues/2447) ## 1.16.1-beta diff --git a/common/src/app/common/geom/shapes/text.cljc b/common/src/app/common/geom/shapes/text.cljc index 992f3c092..a160eb85e 100644 --- a/common/src/app/common/geom/shapes/text.cljc +++ b/common/src/app/common/geom/shapes/text.cljc @@ -22,7 +22,9 @@ (let [points (->> shape :position-data (mapcat (comp gpr/rect->points position-data->rect)))] - (-> points (gpr/points->selrect)))) + (if (empty? points) + (:selrect shape) + (-> points (gpr/points->selrect))))) (defn position-data-bounding-box [shape] From 5605ac2769b7700fb838311cb06442b71e1ecdf4 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 16 Dec 2022 10:28:28 +0100 Subject: [PATCH 03/31] :paperclip: Increment version number. --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 00ff58a87..164087cf8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.16.1-beta +1.16.2-beta From 4e1e67fc3de8463e6af79797f7d5986fdfe89e43 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 12 Dec 2022 15:33:55 +0100 Subject: [PATCH 04/31] :bug: Fix unexpected redirect on invitation acceptation --- frontend/src/app/main.cljs | 1 + frontend/src/app/main/data/users.cljs | 1 + frontend/src/app/main/ui/routes.cljs | 37 ++++++++------------------- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index ce313ce90..5831edea8 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -60,6 +60,7 @@ (rx/merge (rx/of (ev/initialize) (du/initialize-profile)) + (->> stream (rx/filter du/profile-fetched?) (rx/take 1) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 96507a85e..51f962622 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -164,6 +164,7 @@ (swap! storage dissoc :redirect-url) (.replace js/location redirect-url)) (rt/nav' :dashboard-projects {:team-id team-id}))))] + (ptk/reify ::logged-in IDeref (-deref [_] profile) diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index 0f0aefd2f..d6ffde3fe 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -10,9 +10,9 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.users :as du] + [app.main.repo :as rp] [app.main.store :as st] [app.util.router :as rt] - [app.util.storage :refer [storage]] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) @@ -93,32 +93,17 @@ (defn on-navigate [router path] - (let [match (match-path router path) - profile (:profile @storage) - nopath? (or (= path "") (= path "/")) - path-name (-> match :data :name) - authpath? (some #(= path-name %) '(:auth-login - :auth-register - :auth-register-validate - :auth-register-success - :auth-recovery-request - :auth-recovery)) - authed? (and (not (nil? profile)) - (not= (:id profile) uuid/zero))] + (if-let [match (match-path router path)] + (st/emit! (rt/navigated match)) - (cond - (or (and nopath? authed? (nil? match)) - (and authpath? authed?)) - (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})) - - (and (not authed?) (nil? match)) - (st/emit! (rt/nav :auth-login)) - - (nil? match) - (st/emit! (rt/assign-exception {:type :not-found})) - - :else - (st/emit! (rt/navigated match))))) + ;; We just recheck with an additional profile request; this avoids + ;; some race conditions that causes unexpected redirects on + ;; invitations workflows (and probably other cases). + (->> (rp/query! :profile) + (rx/subs (fn [{:keys [id] :as profile}] + (if (= id uuid/zero) + (st/emit! (rt/nav :auth-login)) + (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})))))))) (defn init-routes [] From 797ae22526f4853e1283aca2065fd4c0dd820d54 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 13 Dec 2022 09:50:52 +0100 Subject: [PATCH 05/31] :sparkles: Use the same value for created_at and tracked_at on audit --- backend/src/app/loggers/audit.clj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 55c3339fd..3d1d172b6 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -143,11 +143,13 @@ (defn- persist-event! [pool event] (us/verify! ::event event) - (let [params {:id (uuid/next) + (let [now (dt/now) + params {:id (uuid/next) :name (:name event) :type (:type event) :profile-id (:profile-id event) - :tracked-at (dt/now) + :created-at now + :tracked-at now :ip-addr (:ip-addr event) :props (:props event)}] From c570557203323f75a4dd131e000ccf7b75a84a77 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 13 Dec 2022 09:52:30 +0100 Subject: [PATCH 06/31] :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}] From 36f2ca6bb22e4e3bd0d36954681fc630dc51a3da Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 13 Dec 2022 11:17:47 +0100 Subject: [PATCH 07/31] :sparkles: Add generic (blocking) retry macro And use it on audit handling --- backend/src/app/loggers/audit.clj | 26 +++++++++++++++-------- backend/src/app/rpc/retry.clj | 20 +++++++++--------- backend/src/app/util/retry.clj | 34 +++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 backend/src/app/util/retry.clj diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 3d1d172b6..0d692c275 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -21,6 +21,7 @@ [app.main :as-alias main] [app.metrics :as mtx] [app.tokens :as tokens] + [app.util.retry :as rtry] [app.util.time :as dt] [app.worker :as wrk] [clojure.spec.alpha :as s] @@ -143,22 +144,29 @@ (defn- persist-event! [pool event] (us/verify! ::event event) - (let [now (dt/now) - params {:id (uuid/next) + (let [params {:id (uuid/next) :name (:name event) :type (:type event) :profile-id (:profile-id event) - :created-at now - :tracked-at now :ip-addr (:ip-addr event) :props (:props event)}] (when (contains? cf/flags :audit-log) - (db/insert! pool :audit-log - (-> params - (update :props db/tjson) - (update :ip-addr db/inet) - (assoc :source "backend")))) + + ;; NOTE: this operation may cause primary key conflicts on inserts + ;; because of the timestamp precission (two concurrent requests), in + ;; this case we just retry the operation. + (rtry/with-retry {::rtry/when rtry/conflict-exception? + ::rtry/max-retries 6 + ::rtry/label "persist-audit-log-event"} + (let [now (dt/now)] + (db/insert! pool :audit-log + (-> params + (update :props db/tjson) + (update :ip-addr db/inet) + (assoc :created-at now) + (assoc :tracked-at now) + (assoc :source "backend")))))) (when (and (contains? cf/flags :webhooks) (::webhooks/event? event)) diff --git a/backend/src/app/rpc/retry.clj b/backend/src/app/rpc/retry.clj index ffcb80106..450ab4e9c 100644 --- a/backend/src/app/rpc/retry.clj +++ b/backend/src/app/rpc/retry.clj @@ -5,23 +5,23 @@ ;; Copyright (c) KALEIDOS INC (ns app.rpc.retry - "A fault tolerance helpers. Allow retry some operations that we know - we can retry." + "A fault tolerance RPC middleware. Allow retry some operations that we + know we can retry." (:require [app.common.logging :as l] + [app.util.retry :refer [conflict-exception?]] [app.util.services :as sv] [promesa.core :as p])) (defn conflict-db-insert? "Check if exception matches a insertion conflict on postgresql." [e] - (and (instance? org.postgresql.util.PSQLException e) - (= "23505" (.getSQLState e)))) + (conflict-exception? e)) + +(def always-false (constantly false)) (defn wrap-retry - [_ f {:keys [::matches ::sv/name] - :or {matches (constantly false)} - :as mdata}] + [_ f {:keys [::matches ::sv/name] :or {matches always-false} :as mdata}] (when (::enabled mdata) (l/debug :hint "wrapping retry" :name name)) @@ -29,8 +29,8 @@ (if-let [max-retries (::max-retries mdata)] (fn [cfg params] (letfn [(run [retry] - (-> (f cfg params) - (p/catch (partial handle-error retry)))) + (->> (f cfg params) + (p/merr (partial handle-error retry)))) (handle-error [retry cause] (if (matches cause) @@ -40,6 +40,6 @@ (run current-retry) (throw cause))) (throw cause)))] - (run 0))) + (run 1))) f)) diff --git a/backend/src/app/util/retry.clj b/backend/src/app/util/retry.clj new file mode 100644 index 000000000..666a09f47 --- /dev/null +++ b/backend/src/app/util/retry.clj @@ -0,0 +1,34 @@ +;; 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.util.retry + "A fault tolerance helpers. Allow retry some operations that we know + we can retry." + (:require + [app.common.logging :as l]) + (:import + org.postgresql.util.PSQLException)) + +(defn conflict-exception? + "Check if exception matches a insertion conflict on postgresql." + [e] + (and (instance? PSQLException e) + (= "23505" (.getSQLState ^PSQLException e)))) + +(defmacro with-retry + [{:keys [::when ::max-retries ::label] :or {max-retries 3}} & body] + `(loop [tnum# 1] + (let [result# (try + ~@body + (catch Throwable cause# + (if (and (~when cause#) (<= tnum# ~max-retries)) + ::retry + (throw cause#))))] + (if (= ::retry result#) + (do + (l/warn :hint "retrying operation" :label ~label) + (recur (inc tnum#))) + result#)))) From d35e35acde95e61b418c54378ade6d1d7f020a54 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 13 Dec 2022 11:20:46 +0100 Subject: [PATCH 08/31] :fire: Remove old deprecated method from fonts queries RPC --- backend/src/app/rpc/queries/fonts.clj | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/backend/src/app/rpc/queries/fonts.clj b/backend/src/app/rpc/queries/fonts.clj index ac592ed1a..12fa2d8de 100644 --- a/backend/src/app/rpc/queries/fonts.clj +++ b/backend/src/app/rpc/queries/fonts.clj @@ -14,24 +14,6 @@ [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) -(s/def ::profile-id ::us/uuid) -(s/def ::team-font-variants - (s/keys :req-un [::profile-id ::team-id])) - -(sv/defmethod ::team-font-variants - [{: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) - (db/query conn :team-font-variant - {:team-id team-id - :deleted-at nil}))) - ;; --- Query: Font Variants (s/def ::file-id ::us/uuid) From dbf743d58ab3340926d6d5a472b5dd826cecfda4 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 13 Dec 2022 11:24:19 +0100 Subject: [PATCH 09/31] :paperclip: Add missing doc/added metadata on fonts related RPC methods --- backend/src/app/rpc/queries/fonts.clj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/app/rpc/queries/fonts.clj b/backend/src/app/rpc/queries/fonts.clj index 12fa2d8de..e019b00fb 100644 --- a/backend/src/app/rpc/queries/fonts.clj +++ b/backend/src/app/rpc/queries/fonts.clj @@ -10,6 +10,7 @@ [app.db :as db] [app.rpc.commands.files :as files] [app.rpc.commands.teams :as teams] + [app.rpc.doc :as-alias doc] [app.rpc.queries.projects :as projects] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -30,6 +31,7 @@ (contains? o :project-id))))) (sv/defmethod ::font-variants + {::doc/added "1.7"} [{:keys [pool] :as cfg} {:keys [profile-id team-id file-id project-id] :as params}] (with-open [conn (db/open pool)] (cond From a6b26f05631b0a4e3c7eaf30b054059276a61a18 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 13 Dec 2022 11:34:01 +0100 Subject: [PATCH 10/31] :fire: Remove deprecated RPC methods --- backend/src/app/rpc.clj | 6 +- backend/src/app/rpc/mutations/comments.clj | 123 ------------------ backend/src/app/rpc/mutations/management.clj | 58 --------- .../src/app/rpc/mutations/verify_token.clj | 28 ---- backend/src/app/rpc/queries/comments.clj | 82 ------------ .../backend_tests/rpc_management_test.clj | 18 +-- backend/test/backend_tests/rpc_team_test.clj | 6 +- 7 files changed, 13 insertions(+), 308 deletions(-) delete mode 100644 backend/src/app/rpc/mutations/comments.clj delete mode 100644 backend/src/app/rpc/mutations/management.clj delete mode 100644 backend/src/app/rpc/mutations/verify_token.clj delete mode 100644 backend/src/app/rpc/queries/comments.clj diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 61c59a58a..71ffd447c 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -237,7 +237,6 @@ (->> (sv/scan-ns 'app.rpc.queries.projects 'app.rpc.queries.files 'app.rpc.queries.teams - 'app.rpc.queries.comments 'app.rpc.queries.profile 'app.rpc.queries.viewer 'app.rpc.queries.fonts) @@ -250,13 +249,10 @@ (->> (sv/scan-ns 'app.rpc.mutations.media 'app.rpc.mutations.profile 'app.rpc.mutations.files - 'app.rpc.mutations.comments 'app.rpc.mutations.projects 'app.rpc.mutations.teams - 'app.rpc.mutations.management 'app.rpc.mutations.fonts - 'app.rpc.mutations.share-link - 'app.rpc.mutations.verify-token) + 'app.rpc.mutations.share-link) (map (partial process-method cfg)) (into {})))) diff --git a/backend/src/app/rpc/mutations/comments.clj b/backend/src/app/rpc/mutations/comments.clj deleted file mode 100644 index 6b606ba35..000000000 --- a/backend/src/app/rpc/mutations/comments.clj +++ /dev/null @@ -1,123 +0,0 @@ -;; 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.mutations.comments - (:require - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.db :as db] - [app.rpc.commands.comments :as cmd.comments] - [app.rpc.commands.files :as cmd.files] - [app.rpc.doc :as-alias doc] - [app.rpc.retry :as retry] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) - -;; --- Mutation: Create Comment Thread - -(s/def ::create-comment-thread ::cmd.comments/create-comment-thread) - -(sv/defmethod ::create-comment-thread - {::retry/max-retries 3 - ::retry/matches retry/conflict-db-insert? - ::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-comment-permissions! conn profile-id file-id share-id) - (cmd.comments/create-comment-thread conn params))) - -;; --- Mutation: Update Comment Thread Status - -(s/def ::id ::us/uuid) -(s/def ::share-id (s/nilable ::us/uuid)) - -(s/def ::update-comment-thread-status ::cmd.comments/update-comment-thread-status) - -(sv/defmethod ::update-comment-thread-status - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id share-id] :as params}] - (db/with-atomic [conn pool] - (let [cthr (db/get-by-id conn :comment-thread id {:for-update true})] - (when-not cthr (ex/raise :type :not-found)) - (cmd.files/check-comment-permissions! conn profile-id (:file-id cthr) share-id) - (cmd.comments/upsert-comment-thread-status! conn profile-id (:id cthr))))) - - -;; --- Mutation: Update Comment Thread - -(s/def ::update-comment-thread ::cmd.comments/update-comment-thread) - -(sv/defmethod ::update-comment-thread - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id is-resolved share-id] :as params}] - (db/with-atomic [conn pool] - (let [thread (db/get-by-id conn :comment-thread id {:for-update true})] - (when-not thread - (ex/raise :type :not-found)) - - (cmd.files/check-comment-permissions! conn profile-id (:file-id thread) share-id) - (db/update! conn :comment-thread - {:is-resolved is-resolved} - {:id id}) - nil))) - - -;; --- Mutation: Add Comment - -(s/def ::add-comment ::cmd.comments/create-comment) - -(sv/defmethod ::add-comment - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.comments/create-comment conn params))) - - -;; --- Mutation: Update Comment - -(s/def ::update-comment ::cmd.comments/update-comment) - -(sv/defmethod ::update-comment - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.comments/update-comment conn params))) - - -;; --- Mutation: Delete Comment Thread - -(s/def ::delete-comment-thread ::cmd.comments/delete-comment-thread) - -(sv/defmethod ::delete-comment-thread - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] - (db/with-atomic [conn pool] - (let [thread (db/get-by-id conn :comment-thread id {:for-update true})] - (when-not (= (:owner-id thread) profile-id) - (ex/raise :type :validation :code :not-allowed)) - (db/delete! conn :comment-thread {:id id}) - nil))) - - -;; --- Mutation: Delete comment - -(s/def ::delete-comment ::cmd.comments/delete-comment) - -(sv/defmethod ::delete-comment - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] - (db/with-atomic [conn pool] - (let [comment (db/get-by-id conn :comment id {:for-update true})] - (when-not (= (:owner-id comment) profile-id) - (ex/raise :type :validation :code :not-allowed)) - (db/delete! conn :comment {:id id})))) diff --git a/backend/src/app/rpc/mutations/management.clj b/backend/src/app/rpc/mutations/management.clj deleted file mode 100644 index e29a5e98e..000000000 --- a/backend/src/app/rpc/mutations/management.clj +++ /dev/null @@ -1,58 +0,0 @@ -;; 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.mutations.management - "Move & Duplicate RPC methods for files and projects." - (:require - [app.db :as db] - [app.rpc.commands.management :as cmd.mgm] - [app.rpc.doc :as-alias doc] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) - -;; --- MUTATION: Duplicate File - -(s/def ::duplicate-file ::cmd.mgm/duplicate-file) - -(sv/defmethod ::duplicate-file - {::doc/added "1.2" - ::doc/deprecated "1.16"} - [{:keys [pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.mgm/duplicate-file conn params))) - -;; --- MUTATION: Duplicate Project - -(s/def ::duplicate-project ::cmd.mgm/duplicate-project) - -(sv/defmethod ::duplicate-project - {::doc/added "1.2" - ::doc/deprecated "1.16"} - [{:keys [pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.mgm/duplicate-project conn params))) - -;; --- MUTATION: Move file - -(s/def ::move-files ::cmd.mgm/move-files) - -(sv/defmethod ::move-files - {::doc/added "1.2" - ::doc/deprecated "1.16"} - [{:keys [pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.mgm/move-files conn params))) - -;; --- MUTATION: Move project - -(s/def ::move-project ::cmd.mgm/move-project) - -(sv/defmethod ::move-project - {::doc/added "1.2" - ::doc/deprecated "1.16"} - [{:keys [pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.mgm/move-project conn params))) diff --git a/backend/src/app/rpc/mutations/verify_token.clj b/backend/src/app/rpc/mutations/verify_token.clj deleted file mode 100644 index a8551847b..000000000 --- a/backend/src/app/rpc/mutations/verify_token.clj +++ /dev/null @@ -1,28 +0,0 @@ -;; 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.mutations.verify-token - (:require - [app.db :as db] - [app.rpc.commands.verify-token :refer [process-token]] - [app.rpc.doc :as-alias doc] - [app.tokens :as tokens] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) - -(s/def ::verify-token - (s/keys :req-un [::token] - :opt-un [::profile-id])) - -(sv/defmethod ::verify-token - {:auth false - ::doc/added "1.1" - ::doc/deprecated "1.15"} - [{:keys [pool sprops] :as cfg} {:keys [token] :as params}] - (db/with-atomic [conn pool] - (let [claims (tokens/verify sprops {:token token}) - cfg (assoc cfg :conn conn)] - (process-token cfg params claims)))) diff --git a/backend/src/app/rpc/queries/comments.clj b/backend/src/app/rpc/queries/comments.clj deleted file mode 100644 index fbcb86f03..000000000 --- a/backend/src/app/rpc/queries/comments.clj +++ /dev/null @@ -1,82 +0,0 @@ -;; 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.queries.comments - (:require - [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.util.services :as sv] - [clojure.spec.alpha :as s])) - -(defn decode-row - [{:keys [participants position] :as row}] - (cond-> row - (db/pgpoint? position) (assoc :position (db/decode-pgpoint position)) - (db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants)))) - -;; --- QUERY: Comment Threads - -(s/def ::comment-threads ::cmd.comments/get-comment-threads) - -(sv/defmethod ::comment-threads - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} params] - (with-open [conn (db/open pool)] - (cmd.comments/retrieve-comment-threads conn params))) - -;; --- QUERY: Unread Comment Threads - -(s/def ::unread-comment-threads ::cmd.comments/get-unread-comment-threads) - -(sv/defmethod ::unread-comment-threads - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{: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.comments/retrieve-unread-comment-threads conn params))) - -;; --- QUERY: Single Comment Thread - -(s/def ::comment-thread ::cmd.comments/get-comment-thread) - -(sv/defmethod ::comment-thread - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}] - (with-open [conn (db/open pool)] - (cmd.files/check-comment-permissions! conn profile-id file-id share-id) - (cmd.comments/get-comment-thread conn params))) - -;; --- QUERY: Comments - -(s/def ::comments ::cmd.comments/get-comments) - -(sv/defmethod ::comments - {::doc/added "1.0" - ::doc/deprecated "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id thread-id share-id] :as params}] - (with-open [conn (db/open pool)] - (let [thread (db/get-by-id conn :comment-thread thread-id)] - (cmd.files/check-comment-permissions! conn profile-id (:file-id thread) share-id)) - (cmd.comments/get-comments conn thread-id))) - - -;; --- QUERY: Get file comments users - -(s/def ::file-comments-users ::cmd.comments/get-profiles-for-file-comments) - -(sv/defmethod ::file-comments-users - {::doc/deprecated "1.15" - ::doc/added "1.13"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id]}] - (with-open [conn (db/open pool)] - (cmd.files/check-comment-permissions! conn profile-id file-id share-id) - (cmd.comments/get-file-comments-users conn file-id profile-id))) diff --git a/backend/test/backend_tests/rpc_management_test.clj b/backend/test/backend_tests/rpc_management_test.clj index c1f03838d..5b4f26d05 100644 --- a/backend/test/backend_tests/rpc_management_test.clj +++ b/backend/test/backend_tests/rpc_management_test.clj @@ -53,7 +53,7 @@ :profile-id (:id profile) :file-id (:id file1) :name "file 1 (copy)"} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) @@ -125,7 +125,7 @@ :profile-id (:id profile) :file-id (:id file1) :name "file 1 (copy)"} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) @@ -187,7 +187,7 @@ :profile-id (:id profile) :project-id (:id project) :name "project 1 (copy)"} - out (th/mutation! data)] + out (th/command! data)] ;; Check that result is correct (t/is (nil? (:error out))) @@ -253,7 +253,7 @@ :profile-id (:id profile) :project-id (:id project) :name "project 1 (copy)"} - out (th/mutation! data)] + out (th/command! data)] ;; Check that result is correct (t/is (nil? (:error out))) @@ -317,7 +317,7 @@ :project-id (:id project1) :ids #{(:id file1)}} - out (th/mutation! data) + out (th/command! data) error (:error out)] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) @@ -337,7 +337,7 @@ :project-id (:id project2) :ids #{(:id file1)}} - out (th/mutation! data)] + out (th/command! data)] (t/is (nil? (:error out))) (t/is (nil? (:result out))) @@ -419,7 +419,7 @@ :profile-id (:id profile) :project-id (:id project2) :ids #{(:id file1)}} - out (th/mutation! data)] + out (th/command! data)] (t/is (nil? (:error out))) (t/is (nil? (:result out))) @@ -492,7 +492,7 @@ :profile-id (:id profile) :project-id (:id project2) :ids #{(:id file2)}} - out (th/mutation! data)] + out (th/command! data)] (t/is (nil? (:error out))) (t/is (nil? (:result out))) @@ -578,7 +578,7 @@ :profile-id (:id profile) :project-id (:id project1) :team-id (:id team)} - out (th/mutation! data)] + out (th/command! data)] (t/is (nil? (:error out))) (t/is (nil? (:result out))) diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index 302f80dc5..cababafae 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -179,7 +179,7 @@ :valid-until (dt/in-future "48h")}) (let [data {::th/type :verify-token :token token} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (th/success? out)) (let [result (:result out)] @@ -205,7 +205,7 @@ :valid-until (dt/in-future "48h")}) (let [data {::th/type :verify-token :token token :profile-id (:id profile2)} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (th/success? out)) (let [result (:result out)] @@ -226,7 +226,7 @@ :valid-until (dt/in-future "48h")}) (let [data {::th/type :verify-token :token token :profile-id (:id profile1)} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (not (th/success? out))) (let [edata (-> out :error ex-data)] From f11da06637af50e7beec839e0fe1ab96df371899 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 13 Dec 2022 15:29:43 +0100 Subject: [PATCH 11/31] :tada: Add the ability to copy team invitation link --- backend/src/app/main.clj | 2 +- backend/src/app/rpc/commands/teams.clj | 113 +++++++---- backend/src/app/rpc/commands/verify_token.clj | 2 +- backend/src/app/rpc/mutations/teams.clj | 35 ++-- backend/test/backend_tests/rpc_team_test.clj | 12 +- frontend/src/app/main/data/dashboard.cljs | 34 +++- frontend/src/app/main/ui/dashboard/team.cljs | 182 +++++++++++------- frontend/translations/en.po | 8 + frontend/translations/es.po | 9 + 9 files changed, 262 insertions(+), 135 deletions(-) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index b2145aeb4..c8d3a181a 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -322,7 +322,7 @@ ::http.client/client (ig/ref ::http.client/client) ::db/pool (ig/ref ::db/pool) ::wrk/executor (ig/ref ::wrk/executor) - + ::props (ig/ref :app.setup/props) :pool (ig/ref ::db/pool) :session (ig/ref :app.http.session/manager) :sprops (ig/ref :app.setup/props) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index ec88484e0..5abed23eb 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -15,6 +15,7 @@ [app.db :as db] [app.emails :as eml] [app.loggers.audit :as audit] + [app.main :as-alias main] [app.media :as media] [app.rpc.climit :as climit] [app.rpc.doc :as-alias doc] @@ -260,7 +261,7 @@ (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") + from team_invitation where team_id = ? order by valid_until desc, created_at desc") (defn get-team-invitations [conn team-id] @@ -628,25 +629,37 @@ "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();") + update set role = ?, updated_at = now();") + +(defn- create-invitation-token + [cfg {:keys [expire profile-id team-id member-id member-email role]}] + (tokens/generate (::main/props cfg) + {:iss :team-invitation + :exp expire + :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] + (tokens/generate (::main/props cfg) + {:iss :profile-identity + :profile-id (:id profile) + :exp (dt/in-future {:days 30})})) (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})})] + [{:keys [::conn] :as cfg} {:keys [team profile role email] :as params}] + (let [member (profile/retrieve-profile-data-by-email conn email) + expire (dt/in-future "168h") ;; 7 days + itoken (create-invitation-token cfg {:profile-id (:id profile) + :expire expire + :team-id (:id team) + :member-email (or (:email member) email) + :member-id (:id member) + :role role}) + ptoken (create-profile-identity-token cfg profile)] (when (and member (not (eml/allow-send-emails? conn member))) (ex/raise :type :validation @@ -687,11 +700,10 @@ {: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]) + (:id team) (str/lower email) (name role) expire (name role)]) (eml/send! {::eml/conn conn ::eml/factory eml/invite-to-team - :public-uri (:public-uri cfg) + :public-uri (cf/get :public-uri) :to email :invited-by (:fullname profile) :team (:name team) @@ -727,15 +739,14 @@ :code :profile-is-muted :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) - (let [invitations (->> emails + (let [cfg (assoc cfg ::conn conn) + invitations (->> emails (map (fn [email] - (assoc cfg - :email email - :conn conn - :team team - :profile profile - :role role))) - (map create-invitation))] + {:email (str/lower email) + :team team + :profile profile + :role role})) + (map (partial create-invitation cfg)))] (with-meta (vec invitations) {::audit/props {:invitations (count invitations)}}))))) @@ -743,26 +754,26 @@ ;; --- Mutation: Create Team & Invite Members (s/def ::emails ::us/set-of-valid-emails) -(s/def ::create-team-and-invitations +(s/def ::create-team-with-invitations (s/merge ::create-team (s/keys :req-un [::emails ::role]))) -(sv/defmethod ::create-team-and-invitations +(sv/defmethod ::create-team-with-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)] + profile (db/get-by-id conn :profile profile-id) + cfg (assoc cfg ::conn conn)] ;; Create invitations for all provided emails. - (doseq [email emails] - (create-invitation - (assoc cfg - :conn conn - :team team - :profile profile - :email email - :role role))) + (->> emails + (map (fn [email] + {:team team + :profile profile + :email (str/lower email) + :role role})) + (run! (partial create-invitation cfg))) (-> team (vary-meta assoc ::audit/props {:invitations (count emails)}) @@ -777,6 +788,28 @@ :profile-id profile-id :invitations (count emails)}}))))))) +;; --- Query: get-team-invitation-token + +(s/def ::get-team-invitation-token + (s/keys :req-un [::profile-id ::team-id ::email])) + +(sv/defmethod ::get-team-invitation-token + {::doc/added "1.17"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email] :as params}] + (check-read-permissions! pool profile-id team-id) + (let [invit (-> (db/get pool :team-invitation + {:team-id team-id + :email-to (str/lower email)}) + (update :role keyword)) + member (profile/retrieve-profile-data-by-email pool (:email invit)) + token (create-invitation-token cfg {:team-id (:team-id invit) + :profile-id profile-id + :expire (:expire invit) + :role (:role invit) + :member-id (:id member) + :member-email (or (:email member) (:email-to invit))})] + {:token token})) + ;; --- Mutation: Update invitation role (s/def ::update-team-invitation-role diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 3242a8211..66fce865d 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -129,7 +129,7 @@ [{:keys [conn session] :as cfg} {:keys [profile-id token]} {:keys [member-id team-id member-email] :as claims}] - (us/assert ::team-invitation-claims claims) + (us/verify! ::team-invitation-claims claims) (let [invitation (db/get* conn :team-invitation {:team-id team-id :email-to member-email}) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index d15a376df..650ac1884 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -153,21 +153,20 @@ :code :profile-is-muted :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) - (let [invitations (->> emails + (let [cfg (assoc cfg ::cmd.teams/conn conn) + invitations (->> emails (map (fn [email] - (assoc cfg - :email email - :conn conn - :team team - :profile profile - :role role))) - (map #'cmd.teams/create-invitation))] + {:email (str/lower email) + :team team + :profile profile + :role role})) + (map (partial #'cmd.teams/create-invitation cfg)))] (with-meta (vec invitations) {::audit/props {:invitations (count invitations)}}))))) ;; --- Mutation: Create Team & Invite Members -(s/def ::create-team-and-invite-members ::cmd.teams/create-team-and-invitations) +(s/def ::create-team-and-invite-members ::cmd.teams/create-team-with-invitations) (sv/defmethod ::create-team-and-invite-members {::doc/added "1.0" @@ -175,17 +174,17 @@ [{:keys [::db/pool] :as cfg} {:keys [profile-id emails role] :as params}] (db/with-atomic [conn pool] (let [team (cmd.teams/create-team conn params) - profile (db/get-by-id conn :profile profile-id)] + profile (db/get-by-id conn :profile profile-id) + cfg (assoc cfg ::cmd.teams/conn conn)] ;; Create invitations for all provided emails. - (doseq [email emails] - (#'cmd.teams/create-invitation - (assoc cfg - :conn conn - :team team - :profile profile - :email email - :role role))) + (->> emails + (map (fn [email] + {:team team + :profile profile + :email (str/lower email) + :role role})) + (run! (partial #'cmd.teams/create-invitation cfg))) (-> team (vary-meta assoc ::audit/props {:invitations (count emails)}) diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index cababafae..de66c2436 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -63,6 +63,16 @@ (t/is (th/success? out)) (t/is (= 1 (:call-count (deref mock))))) + ;; get invitation token + (let [params {::th/type :get-team-invitation-token + :profile-id (:id profile1) + :team-id (:id team) + :email "foo@bar.com"} + out (th/command! params)] + (t/is (th/success? out)) + (let [result (:result out)] + (contains? result :token))) + ;; invite user with bounce (th/reset-mock! mock) @@ -235,8 +245,6 @@ ))) - - (t/deftest invite-team-member-with-email-verification-disabled (with-mocks [mock {:target 'app.emails/send! :return nil}] (let [profile1 (th/create-profile* 1 {:is-active true}) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index ca05fbfa2..2aeb562fc 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.spec :as us] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.events :as ev] [app.main.data.fonts :as df] [app.main.data.media :as di] @@ -18,6 +19,7 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [app.util.timers :as tm] + [app.util.webapi :as wapi] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) @@ -403,7 +405,7 @@ params {:name name :emails #{emails} :role role}] - (->> (rp/cmd! :create-team-and-invitations params) + (->> (rp/cmd! :create-team-with-invitations params) (rx/tap on-success) (rx/map team-created) (rx/catch on-error)))))) @@ -509,6 +511,36 @@ (rx/tap on-success) (rx/catch on-error)))))) + +(defn copy-invitation-link + [{:keys [email team-id] :as params}] + (us/assert! ::us/email email) + (us/assert! ::us/uuid team-id) + + (ptk/reify ::copy-invitation-link + IDeref + (-deref [_] {:email email :team-id team-id}) + + + ptk/WatchEvent + (watch [_ state _] + (let [{:keys [on-success on-error] + :or {on-success identity + on-error rx/throw}} (meta params) + router (:router state)] + + (->> (rp/cmd! :get-team-invitation-token params) + (rx/map (fn [params] + (rt/resolve router :auth-verify-token {} params))) + (rx/map (fn [fragment] + (assoc @cf/public-uri :fragment fragment))) + (rx/tap (fn [uri] + (wapi/write-to-clipboard (str uri)))) + (rx/tap on-success) + (rx/ignore) + (rx/catch on-error)))))) + + (defn update-team-invitation-role [{:keys [email team-id role] :as params}] (us/assert! ::us/email email) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 8997245e1..1c5a59f66 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -448,81 +448,113 @@ :pending (= status :pending))} [:span.status-label (tr status-label)]])) -(mf/defc invitation-actions [{:keys [can-modify? delete resend] :as props}] - (let [show? (mf/use-state false)] - (when can-modify? - [:* - [:span.icon {:on-click #(reset! show? true)} [i/actions]] - [:& dropdown {:show @show? - :on-close #(reset! show? false)} - [:ul.dropdown.actions-dropdown - [:li {:on-click resend} (tr "labels.resend-invitation")] - [:li {:on-click delete} (tr "labels.delete-invitation")]]]]))) +(mf/defc invitation-actions + [{:keys [invitation team] :as props}] + (let [show? (mf/use-state false) + + team-id (:id team) + email (:email invitation) + role (:role invitation) + + on-resend-success + (mf/use-fn + (fn [] + (st/emit! (msg/success (tr "notifications.invitation-email-sent")) + (modal/hide)))) + + on-copy-success + (mf/use-fn + (fn [] + (st/emit! (msg/success (tr "notifications.invitation-link-copied")) + (modal/hide)))) + + on-error + (mf/use-fn + (mf/deps email) + (fn [{:keys [type code] :as error}] + (cond + (and (= :validation type) + (= :profile-is-muted code)) + (rx/of (msg/error (tr "errors.profile-is-muted"))) + + (and (= :validation type) + (= :member-is-muted code)) + (rx/of (msg/error (tr "errors.member-is-muted"))) + + (and (= :validation type) + (= :email-has-permanent-bounces code)) + (rx/of (msg/error (tr "errors.email-has-permanent-bounces" email))) + + :else + (rx/throw error)))) + + delete-fn + (mf/use-fn + (mf/deps email team-id) + (fn [] + (let [params {:email email :team-id team-id} + mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}] + (st/emit! (dd/delete-team-invitation (with-meta params mdata)))))) + + resend-fn + (mf/use-fn + (mf/deps email team-id) + (fn [] + (let [params (with-meta {:emails [email] + :team-id team-id + :resend? true + :role role} + {:on-success on-resend-success + :on-error on-error})] + (st/emit! + (-> (dd/invite-team-members params) + (with-meta {::ev/origin :team})))))) + + copy-fn + (mf/use-fn + (mf/deps email team-id) + (fn [] + (let [params (with-meta {:email email :team-id team-id} + {:on-success on-copy-success + :on-error on-error})] + (prn "KKK1") + (st/emit! + (-> (dd/copy-invitation-link params) + (with-meta {::ev/origin :team}))))))] + + + [:* + [:span.icon {:on-click #(reset! show? true)} [i/actions]] + [:& dropdown {:show @show? + :on-close #(reset! show? false)} + [:ul.dropdown.actions-dropdown + [:li {:on-click copy-fn} (tr "labels.copy-invitation-link")] + [:li {:on-click resend-fn} (tr "labels.resend-invitation")] + [:li {:on-click delete-fn} (tr "labels.delete-invitation")]]]])) (mf/defc invitation-row {::mf/wrap [mf/memo]} [{:keys [invitation can-invite? team] :as props}] - (let [expired? (:expired invitation) - email (:email invitation) - invitation-role (:role invitation) - status (if expired? - :expired - :pending) - - on-success - #(st/emit! (msg/success (tr "notifications.invitation-email-sent")) - (modal/hide) - (dd/fetch-team-invitations)) - - - on-error - (fn [email {:keys [type code] :as error}] - (cond - (and (= :validation type) - (= :profile-is-muted code)) - (msg/error (tr "errors.profile-is-muted")) - - (and (= :validation type) - (= :member-is-muted code)) - (msg/error (tr "errors.member-is-muted")) - - (and (= :validation type) - (= :email-has-permanent-bounces code)) - (msg/error (tr "errors.email-has-permanent-bounces" email)) - - :else - (msg/error (tr "errors.generic")))) + (let [expired? (:expired invitation) + email (:email invitation) + role (:role invitation) + status (if expired? :expired :pending) change-rol - (fn [role] - (let [params {:email email :team-id (:id team) :role role} - mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}] - (st/emit! (dd/update-team-invitation-role (with-meta params mdata))))) + (mf/use-fn + (mf/deps team email) + (fn [role] + (let [params {:email email :team-id (:id team) :role role} + mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}] + (st/emit! (dd/update-team-invitation-role (with-meta params mdata))))))] - delete-invitation - (fn [] - (let [params {:email email :team-id (:id team)} - mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}] - (st/emit! (dd/delete-team-invitation (with-meta params mdata))))) - - resend-invitation - (fn [] - (let [params {:emails [email] - :team-id (:id team) - :resend? true - :role invitation-role} - mdata {:on-success on-success - :on-error (partial on-error email)}] - (st/emit! (-> (dd/invite-team-members (with-meta params mdata)) - (with-meta {::ev/origin :team})) - (dd/fetch-team-invitations))))] [:div.table-row [:div.table-field.mail email] [:div.table-field.roles [:& invitation-role-selector {:can-invite? can-invite? - :role invitation-role + :role role :status status :change-to-editor (partial change-rol :editor) :change-to-admin (partial change-rol :admin)}]] @@ -530,20 +562,22 @@ [:div.table-field.status [:& invitation-status-badge {:status status}]] [:div.table-field.actions - [:& invitation-actions - {:can-modify? can-invite? - :delete delete-invitation - :resend resend-invitation}]]])) + (when can-invite? + [:& invitation-actions + {:invitation invitation + :team team}])]])) -(mf/defc empty-invitation-table [can-invite?] +(mf/defc empty-invitation-table + [{:keys [can-invite?] :as props}] [:div.empty-invitations [:span (tr "labels.no-invitations")] - (when (:can-invite? can-invite?) [:span (tr "labels.no-invitations-hint")])]) + (when can-invite? + [:span (tr "labels.no-invitations-hint")])]) (mf/defc invitation-section [{:keys [team invitations] :as props}] - (let [owner? (get-in team [:permissions :is-owner]) - admin? (get-in team [:permissions :is-admin]) + (let [owner? (dm/get-in team [:permissions :is-owner]) + admin? (dm/get-in team [:permissions :is-admin]) can-invite? (or owner? admin?)] [:div.dashboard-table.invitations @@ -555,7 +589,11 @@ [:& empty-invitation-table {:can-invite? can-invite?}] [:div.table-rows (for [invitation invitations] - [:& invitation-row {:key (:email invitation) :invitation invitation :can-invite? can-invite? :team team}])])])) + [:& invitation-row + {:key (:email invitation) + :invitation invitation + :can-invite? can-invite? + :team team}])])])) (mf/defc team-invitations-page [{:keys [team] :as props}] @@ -568,7 +606,7 @@ (tr "dashboard.your-penpot") (:name team))))) - (mf/with-effect + (mf/with-effect [] (st/emit! (dd/fetch-team-invitations))) [:* diff --git a/frontend/translations/en.po b/frontend/translations/en.po index de91460a8..ba88e0d55 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1436,6 +1436,10 @@ msgstr "Rename team" msgid "labels.resend-invitation" msgstr "Resend invitation" +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.copy-invitation-link" +msgstr "Copy invitation link" + #: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Retry" @@ -1957,6 +1961,10 @@ msgstr "Update a component in a shared library" msgid "notifications.invitation-email-sent" msgstr "Invitation sent successfully" +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-link-copied" +msgstr "Invitation link copied" + #: src/app/main/ui/settings/delete_account.cljs msgid "notifications.profile-deletion-not-allowed" msgstr "You can't delete you profile. Reassign your teams before proceed." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index f06309bb0..2c4076331 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1606,6 +1606,10 @@ msgstr "Renombra el equipo" msgid "labels.resend-invitation" msgstr "Reenviar invitacion" +#: src/app/main/ui/dashboard/team.cljs +msgid "labels.copy-invitation-link" +msgstr "Copiar link de invitación" + #: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" msgstr "Reintentar" @@ -2172,6 +2176,11 @@ msgstr "Actualizar un componente en biblioteca" msgid "notifications.invitation-email-sent" msgstr "Invitación enviada con éxito" +#: src/app/main/ui/dashboard/team.cljs +msgid "notifications.invitation-link-copied" +msgstr "Enlace de invitacion copiado" + + #: src/app/main/ui/settings/delete_account.cljs msgid "notifications.profile-deletion-not-allowed" msgstr "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir." From e50ecd70c6a8e7090d344063dc17c47e48d3b779 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 13 Dec 2022 15:40:35 +0100 Subject: [PATCH 12/31] :paperclip: Add some cosmetic changes to kondo config --- .clj-kondo/config.edn | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index e655c6e4c..5989d2c52 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -45,6 +45,15 @@ :redundant-do {:level :off} + :earmuffed-var-not-dynamic + {:level :off} + + :dynamic-var-not-earmuffed + {:level :off} + + :used-underscored-binding + {:level :warning} + :unused-binding {:exclude-destructured-as true :exclude-destructured-keys-in-fn-args false From 801d926946db6f44b4e0eedc23f7cf6a60e41607 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 14 Dec 2022 14:14:01 +0100 Subject: [PATCH 13/31] :lipstick: Fix linter issues --- .../src/app/main/ui/workspace/context_menu.cljs | 4 ++-- .../sidebar/options/menus/component.cljs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 00d120793..b2f89ff10 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -432,7 +432,7 @@ do-update-component-in-bulk #(st/emit! (dwl/update-component-in-bulk component-shapes component-file)) do-restore-component #(st/emit! (dwl/restore-component component-file component-id)) - _do-update-remote-component + do-update-remote-component #(st/emit! (modal/show {:type :confirm :message "" @@ -516,7 +516,7 @@ [:& menu-entry {:title (tr "workspace.shape.menu.reset-overrides") :on-click do-reset-component}] [:& menu-entry {:title (tr "workspace.shape.menu.update-main") - :on-click _do-update-remote-component}] + :on-click do-update-remote-component}] [:& menu-entry {:title (tr "workspace.shape.menu.go-main") :on-click do-navigate-component-file}]])))]) [:& menu-separator]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 392b135ea..d78a034f0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -57,7 +57,7 @@ do-detach-component #(st/emit! (dwl/detach-component id)) - _do-reset-component + do-reset-component #(st/emit! (dwl/reset-component id)) do-update-component @@ -66,7 +66,7 @@ do-restore-component #(st/emit! (dwl/restore-component library-id component-id)) - _do-update-remote-component + do-update-remote-component #(st/emit! (modal/show {:type :confirm :message "" @@ -100,27 +100,27 @@ ;; app/main/ui/workspace/context_menu.cljs [:& context-menu {:on-close on-menu-close :show (:menu-open @local) - :options + :options (if main-instance? [[(tr "workspace.shape.menu.show-in-assets") do-show-in-assets]] (if local-component? (if is-dangling? [[(tr "workspace.shape.menu.detach-instance") do-detach-component] - [(tr "workspace.shape.menu.reset-overrides") _do-reset-component] + [(tr "workspace.shape.menu.reset-overrides") do-reset-component] (when components-v2 [(tr "workspace.shape.menu.restore-main") do-restore-component])] [[(tr "workspace.shape.menu.detach-instance") do-detach-component] - [(tr "workspace.shape.menu.reset-overrides") _do-reset-component] + [(tr "workspace.shape.menu.reset-overrides") do-reset-component] [(tr "workspace.shape.menu.update-main") do-update-component] [(tr "workspace.shape.menu.show-main") do-show-component]]) (if is-dangling? [[(tr "workspace.shape.menu.detach-instance") do-detach-component] - [(tr "workspace.shape.menu.reset-overrides") _do-reset-component] + [(tr "workspace.shape.menu.reset-overrides") do-reset-component] (when components-v2 [(tr "workspace.shape.menu.restore-main") do-restore-component])] [[(tr "workspace.shape.menu.detach-instance") do-detach-component] - [(tr "workspace.shape.menu.reset-overrides") _do-reset-component] - [(tr "workspace.shape.menu.update-main") _do-update-remote-component] + [(tr "workspace.shape.menu.reset-overrides") do-reset-component] + [(tr "workspace.shape.menu.update-main") do-update-remote-component] [(tr "workspace.shape.menu.go-main") do-navigate-component-file]])))}]]]]]))) From 350e4a1d1bfcc42235f6bfa6dd93f24055daa124 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 14 Dec 2022 14:15:42 +0100 Subject: [PATCH 14/31] :sparkles: Improve default update-file webhook batch timeout --- backend/src/app/loggers/audit.clj | 1 - backend/src/app/rpc/commands/files/update.clj | 2 +- frontend/src/app/main/ui/workspace/context_menu.cljs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 0d692c275..853e9709c 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -152,7 +152,6 @@ :props (:props event)}] (when (contains? cf/flags :audit-log) - ;; NOTE: this operation may cause primary key conflicts on inserts ;; because of the timestamp precission (two concurrent requests), in ;; this case we just retry the operation. diff --git a/backend/src/app/rpc/commands/files/update.clj b/backend/src/app/rpc/commands/files/update.clj index 520df2897..264d29615 100644 --- a/backend/src/app/rpc/commands/files/update.clj +++ b/backend/src/app/rpc/commands/files/update.clj @@ -131,7 +131,7 @@ {::climit/queue :update-file ::climit/key-fn :id ::webhooks/event? true - ::webhooks/batch-timeout (dt/duration "2s") + ::webhooks/batch-timeout (dt/duration "2m") ::webhooks/batch-key webhook-batch-keyfn ::doc/added "1.17"} [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index b2f89ff10..eda6e725b 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -386,7 +386,7 @@ [:& menu-entry {:title (tr "workspace.shape.menu.add-flex") :shortcut (sc/get-tooltip :toogle-layout-flex) :on-click add-flex}]] - + is-flex-container? [:* [:& menu-separator] From 710878a667a9a433b00a527a4af9139201703c86 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 14 Dec 2022 16:21:23 +0100 Subject: [PATCH 15/31] :sparkles: Improve webhook URI validation --- backend/src/app/http/client.clj | 8 ++++++-- backend/src/app/loggers/webhooks.clj | 3 +++ backend/src/app/rpc/commands/webhooks.clj | 7 ++++--- common/src/app/common/spec.cljc | 2 +- .../resources/styles/main/partials/dashboard-team.scss | 8 +++++--- frontend/src/app/main/data/dashboard.cljs | 4 +++- frontend/src/app/main/ui/dashboard/team.cljs | 4 +++- frontend/translations/en.po | 9 ++++++--- frontend/translations/es.po | 3 +++ 9 files changed, 34 insertions(+), 14 deletions(-) diff --git a/backend/src/app/http/client.clj b/backend/src/app/http/client.clj index 9e0be572a..542de4d18 100644 --- a/backend/src/app/http/client.clj +++ b/backend/src/app/http/client.clj @@ -11,7 +11,8 @@ [app.worker :as wrk] [clojure.spec.alpha :as s] [integrant.core :as ig] - [java-http-clj.core :as http]) + [java-http-clj.core :as http] + [promesa.core :as p]) (:import java.net.http.HttpClient)) @@ -34,7 +35,10 @@ (us/assert! ::client client) (if sync? (http/send req {:client client :as response-type}) - (http/send-async req {:client client :as response-type})))) + (try + (http/send-async req {:client client :as response-type}) + (catch Throwable cause + (p/rejected cause)))))) (defn req! "A convencience toplevel function for gradual migration to a new API diff --git a/backend/src/app/loggers/webhooks.clj b/backend/src/app/loggers/webhooks.clj index b05b81558..55a034baa 100644 --- a/backend/src/app/loggers/webhooks.clj +++ b/backend/src/app/loggers/webhooks.clj @@ -169,6 +169,9 @@ (instance? java.net.ConnectException cause) "connection-error" + (instance? java.lang.IllegalArgumentException cause) + "invalid-uri" + (instance? java.net.http.HttpConnectTimeoutException cause) "timeout" )) diff --git a/backend/src/app/rpc/commands/webhooks.clj b/backend/src/app/rpc/commands/webhooks.clj index 454dfee42..13f7578d4 100644 --- a/backend/src/app/rpc/commands/webhooks.clj +++ b/backend/src/app/rpc/commands/webhooks.clj @@ -74,7 +74,8 @@ (when (>= total max-hooks-for-team) (ex/raise :type :restriction :code :webhooks-quote-reached - :hint (str/ffmt "can't create more than % webhooks per team" max-hooks-for-team))))) + :hint (str/ffmt "can't create more than % webhooks per team" + max-hooks-for-team))))) (defn- insert-webhook! [{:keys [::db/pool]} {:keys [team-id uri mtype is-active] :as params}] @@ -99,8 +100,8 @@ {::doc/added "1.17"} [{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id team-id] :as params}] (check-edition-permissions! pool profile-id team-id) - (->> (validate-quotes! cfg params) - (p/fmap executor (fn [_] (validate-webhook! cfg nil params))) + (validate-quotes! cfg params) + (->> (validate-webhook! cfg nil params) (p/fmap executor (fn [_] (insert-webhook! cfg params))))) (s/def ::update-webhook diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index d81e996c2..1dd0eff35 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -135,7 +135,7 @@ (letfn [(conformer [s] (cond (u/uri? s) s - (string? s) (u/uri s) + (string? s) (u/uri (str/trim s)) :else ::s/invalid)) (unformer [v] (dm/str v))] diff --git a/frontend/resources/styles/main/partials/dashboard-team.scss b/frontend/resources/styles/main/partials/dashboard-team.scss index 88c626550..0847c8988 100644 --- a/frontend/resources/styles/main/partials/dashboard-team.scss +++ b/frontend/resources/styles/main/partials/dashboard-team.scss @@ -199,10 +199,12 @@ } } - &.uri, + &.uri { + flex-grow: 1; + } + &.active { - width: 48%; - min-width: 300px; + min-width: 100px; } &.last-delivery { diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 2aeb562fc..8e84b6a21 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -620,7 +620,9 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state) - params (assoc params :team-id team-id) + params (-> params + (assoc :team-id team-id) + (update :uri str)) {:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 1c5a59f66..eada3b00a 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -620,7 +620,7 @@ ;; WEBHOOKS SECTION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::uri ::us/not-empty-string) +(s/def ::uri ::us/uri) (s/def ::mtype ::us/not-empty-string) (s/def ::webhook-form (s/keys :req-un [::uri ::mtype])) @@ -657,6 +657,8 @@ (let [message (cond (= hint "unknown") (tr "errors.webhooks.unexpected") + (= hint "invalid-uri") + (tr "errors.webhooks.invalid-uri") (= hint "ssl-validation-error") (tr "errors.webhooks.ssl-validation") (= hint "timeout") diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ba88e0d55..1c806d08c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -696,6 +696,9 @@ msgstr "Webhook updated successfully." msgid "dashboard.webhooks.create.success" msgstr "Webhook created successfully." +msgid "webhooks.last-delivery.success" +msgstr "Last delivery was successfull." + msgid "errors.webhooks.unexpected" msgstr "Unexpected error on validating" @@ -705,15 +708,15 @@ msgstr "Timeout" msgid "errors.webhooks.connection" msgstr "Connection error, url not reacheable" -msgid "webhooks.last-delivery.success" -msgstr "Last delivery was successfull." - msgid "errors.webhooks.last-delivery" msgstr "Last delivery was not successfull." msgid "errors.webhooks.ssl-validation" msgstr "Error on SSL validation." +msgid "errors.webhooks.invalid-uri" +msgstr "URL does not passes validation." + msgid "errors.webhooks.unexpected-status" msgstr "Unexpected status %s" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 2c4076331..5cf167ec1 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -761,6 +761,9 @@ msgstr "Error en la validación SSL." msgid "errors.webhooks.unexpected-status" msgstr "Estado inesperado %s" +msgid "errors.webhooks.invalid-uri" +msgstr "La URL no pasa la validacion." + #: src/app/main/ui/alert.cljs msgid "ds.alert-ok" msgstr "Ok" From 8f2a02ae72e724eeb81837603b2fa773b1b369d0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 15 Dec 2022 08:18:29 +0100 Subject: [PATCH 16/31] :tada: Add webhooks to the API doc --- .../app/templates/api-doc-entry.tmpl | 19 +++++++++++++------ backend/src/app/rpc/doc.clj | 3 +++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/resources/app/templates/api-doc-entry.tmpl b/backend/resources/app/templates/api-doc-entry.tmpl index 97ce8a507..43af67c24 100644 --- a/backend/resources/app/templates/api-doc-entry.tmpl +++ b/backend/resources/app/templates/api-doc-entry.tmpl @@ -6,14 +6,21 @@
{% if item.deprecated %} - Deprecated: - since v{{item.deprecated}}, + DEPRECATED + + {% endif %} + + {% if item.auth %} + + AUTH + + {% endif %} + + {% if item.webhook %} + + WEBHOOK {% endif %} - - Auth: - {% if item.auth %}YES{% else %}NO{% endif %} -