From 4105692dee5682f70481a626bab4dcde4baf4537 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 7 Apr 2020 14:20:29 +0200 Subject: [PATCH] :tada: Add backend code for share token handling. --- .../resources/migrations/0003.projects.sql | 6 + .../src/uxbox/services/mutations/pages.clj | 36 +++ backend/src/uxbox/services/queries/viewer.clj | 47 +--- .../tests/uxbox/tests/test_common_pages.clj | 258 ++++++++++-------- .../tests/uxbox/tests/test_services_pages.clj | 12 + .../uxbox/tests/test_services_viewer.clj | 102 +++++++ 6 files changed, 304 insertions(+), 157 deletions(-) create mode 100644 backend/tests/uxbox/tests/test_services_viewer.clj diff --git a/backend/resources/migrations/0003.projects.sql b/backend/resources/migrations/0003.projects.sql index 3b3516b1fe..652300e60a 100644 --- a/backend/resources/migrations/0003.projects.sql +++ b/backend/resources/migrations/0003.projects.sql @@ -124,6 +124,8 @@ CREATE TABLE page ( version bigint NOT NULL DEFAULT 0, revn bigint NOT NULL DEFAULT 0, + share_token text NULL DEFAULT NULL, + ordering smallint NOT NULL, name text NOT NULL, @@ -133,6 +135,10 @@ CREATE TABLE page ( CREATE INDEX page__file_id__idx ON page(file_id); +ALTER TABLE page + ALTER COLUMN data SET STORAGE EXTERNAL, + ALTER COLUMN share_token SET STORAGE PLAIN; + CREATE FUNCTION handle_page_update() RETURNS TRIGGER AS $pagechange$ DECLARE diff --git a/backend/src/uxbox/services/mutations/pages.clj b/backend/src/uxbox/services/mutations/pages.clj index 69603829ea..0cf60aa9e2 100644 --- a/backend/src/uxbox/services/mutations/pages.clj +++ b/backend/src/uxbox/services/mutations/pages.clj @@ -106,6 +106,42 @@ +;; --- Mutation: Generate Share Token + +(declare assign-page-share-token) + +(s/def ::generate-page-share-token + (s/keys :req-un [::id])) + +(sm/defmutation ::generate-page-share-token + [{:keys [id] :as params}] + (let [token (-> (sodi.prng/random-bytes 16) + (sodi.util/bytes->b64s))] + (db/with-atomic [conn db/pool] + (assign-page-share-token conn id token)))) + +(def ^:private sql:update-page-share-token + "update page set share_token = $2 where id = $1") + +(defn- assign-page-share-token + [conn id token] + (-> (db/query-one conn [sql:update-page-share-token id token]) + (p/then (fn [_] {:id id :share-token token})))) + + + +;; --- Mutation: Clear Share Token + +(s/def ::clear-page-share-token + (s/keys :req-un [::id])) + +(sm/defmutation ::clear-page-share-token + [{:keys [id] :as params}] + (db/with-atomic [conn db/pool] + (assign-page-share-token conn id nil))) + + + ;; --- Mutation: Update Page ;; A generic, Changes based (granular) page update method. diff --git a/backend/src/uxbox/services/queries/viewer.clj b/backend/src/uxbox/services/queries/viewer.clj index 5e7f24e85b..fc6b2abf50 100644 --- a/backend/src/uxbox/services/queries/viewer.clj +++ b/backend/src/uxbox/services/queries/viewer.clj @@ -44,49 +44,24 @@ [conn id] (db/query-one conn [sql:project id])) -(s/def ::viewer-bundle-by-page-id - (s/keys :req-un [::profile-id ::page-id])) +(s/def ::share-token ::us/string) +(s/def ::viewer-bundle + (s/keys :req-un [::page-id] + :opt-un [::profile-id ::share-token])) -(sq/defquery ::viewer-bundle-by-page-id - [{:keys [profile-id page-id]}] +(sq/defquery ::viewer-bundle + [{:keys [profile-id page-id share-token] :as params}] (db/with-atomic [conn db/pool] (p/let [page (pages/retrieve-page conn page-id) file (files/retrieve-file conn (:file-id page)) images (files/retrieve-file-images conn page) project (retrieve-project conn (:project-id file))] - (files/check-edition-permissions! conn profile-id (:file-id page)) + (if (string? share-token) + (when (not= share-token (:share-token page)) + (ex/raise :type :validation + :code :not-authorized)) + (files/check-edition-permissions! conn profile-id (:file-id page))) {:page page :file file :images images :project project}))) - - -;; --- Query: Viewer Bundle (By Share ID) - -(declare retrieve-page-by-share-id) - -(s/def ::viewer-bundle-by-share-id - (s/keys :req-un [::share-id] - :opt-un [::profile-id])) - -(sq/defquery ::viewer-bundle-by-share-id - [{:keys [share-id]}] - (db/with-atomic [conn db/pool] - (p/let [page (retrieve-page-by-share-id conn share-id) - file (files/retrieve-file conn (:file-id page)) - images (files/retrieve-file-images conn page) - project (retrieve-project conn (:project-id file))] - {:page page - :file file - :images images - :project project}))) - -(def ^:private sql:page-by-share-id - "select p.* from page as p where share_id=$1") - -(defn- retrieve-page-by-share-id - [conn share-id] - (-> (db/query-one conn [sql:page-by-share-id share-id]) - (p/then' su/raise-not-found-if-nil) - (p/then' pages/decode-row))) - diff --git a/backend/tests/uxbox/tests/test_common_pages.clj b/backend/tests/uxbox/tests/test_common_pages.clj index a2e210d527..7d3347d49d 100644 --- a/backend/tests/uxbox/tests/test_common_pages.clj +++ b/backend/tests/uxbox/tests/test_common_pages.clj @@ -13,151 +13,167 @@ [uxbox.util.uuid :as uuid] [uxbox.tests.helpers :as th])) -(t/deftest process-change-add-shape +(t/deftest process-change-add-obj-1 (let [data cp/default-page-data id (uuid/next) - chg {:type :add-shape + chg {:type :add-obj :id id - :session-id (uuid/next) - :shape {:id id - :type :rect - :name "rect"}} + :frame-id uuid/zero + :obj {:id id + :frame-id uuid/zero + :type :rect + :name "rect"}} res (cp/process-changes data [chg])] - (t/is (= 1 (count (:shapes res)))) - (t/is (= 0 (count (:canvas res)))) + (t/is (= 2 (count (:objects res)))) + (t/is (= (:obj chg) (get-in res [:objects id]))) + (t/is (= [id] (get-in res [:objects uuid/zero :shapes]))))) - (t/is (= id (get-in res [:shapes 0]))) - (t/is (= (:shape chg) - (get-in res [:shapes-by-id id]))))) - -(t/deftest process-change-add-canvas +(t/deftest process-change-mod-obj (let [data cp/default-page-data - id (uuid/next) - chg {:type :add-canvas - :id id - :session-id (uuid/next) - :shape {:id id - :type :rect - :name "rect"}} + chg {:type :mod-obj + :id uuid/zero + :operations [{:type :set + :attr :name + :val "foobar"}]} res (cp/process-changes data [chg])] - (t/is (= 0 (count (:shapes res)))) - (t/is (= 1 (count (:canvas res)))) - - (t/is (= id (get-in res [:canvas 0]))) - (t/is (= (:shape chg) - (get-in res [:shapes-by-id id]))))) + (t/is (= "foobar" (get-in res [:objects uuid/zero :name]))))) -(t/deftest process-change-mod-shape +(t/deftest process-change-del-obj-1 (let [id (uuid/next) - data (merge cp/default-page-data - {:shapes [id] - :shapes-by-id {id {:id id - :type :rect - :name "rect"}}}) - - chg {:type :mod-shape - :id id - :session-id (uuid/next) - :operations [[:set :name "foobar"]]} + data (-> cp/default-page-data + (assoc-in [:objects uuid/zero :shapes] [id]) + (assoc-in [:objects id] {:id id + :frame-id uuid/zero + :type :rect + :name "rect"})) + chg {:type :del-obj + :id id} res (cp/process-changes data [chg])] - (t/is (= 1 (count (:shapes res)))) - (t/is (= 0 (count (:canvas res)))) - (t/is (= "foobar" - (get-in res [:shapes-by-id id :name]))))) + (t/is (= 1 (count (:objects res)))) + (t/is (= [] (get-in res [:objects uuid/zero :shapes]))))) -(t/deftest process-change-mod-opts - (t/testing "mod-opts add" - (let [data cp/default-page-data - chg {:type :mod-opts - :session-id (uuid/next) - :operations [[:set :foo "bar"]]} - res (cp/process-changes data [chg])] - - (t/is (= 0 (count (:shapes res)))) - (t/is (= 0 (count (:canvas res)))) - (t/is (empty? (:shapes-by-id res))) - (t/is (= "bar" (get-in res [:options :foo]))))) - - (t/testing "mod-opts set nil" - (let [data (merge cp/default-page-data - {:options {:foo "bar"}}) - chg {:type :mod-opts - :session-id (uuid/next) - :operations [[:set :foo nil]]} - res (cp/process-changes data [chg])] - - (t/is (= 0 (count (:shapes res)))) - (t/is (= 0 (count (:canvas res)))) - (t/is (empty? (:shapes-by-id res))) - (t/is (not (contains? (:options res) :foo))))) - ) - - -(t/deftest process-change-del-shape +(t/deftest process-change-del-obj-2 (let [id (uuid/next) - data (merge cp/default-page-data - {:shapes [id] - :shapes-by-id {id {:id id - :type :rect - :name "rect"}}}) - chg {:type :del-shape - :id id - :session-id (uuid/next)} + data (-> cp/default-page-data + (assoc-in [:objects uuid/zero :shapes] [id]) + (assoc-in [:objects id] {:id id + :frame-id uuid/zero + :type :rect + :name "rect"})) + chg {:type :del-obj + :id uuid/zero} res (cp/process-changes data [chg])] + (t/is (= 0 (count (:objects res)))))) - (t/is (= 0 (count (:shapes res)))) - (t/is (= 0 (count (:canvas res)))) - (t/is (empty? (:shapes-by-id res))))) - -(t/deftest process-change-del-canvas - (let [id (uuid/next) - data (merge cp/default-page-data - {:canvas [id] - :shapes-by-id {id {:id id - :type :canvas - :name "rect"}}}) - chg {:type :del-canvas - :id id - :session-id (uuid/next)} - res (cp/process-changes data [chg])] - - (t/is (= 0 (count (:shapes res)))) - (t/is (= 0 (count (:canvas res)))) - (t/is (empty? (:shapes-by-id res))))) - - -(t/deftest process-change-mov-shape +(t/deftest process-change-mod-obj-abs-order (let [id1 (uuid/next) id2 (uuid/next) id3 (uuid/next) - data (merge cp/default-page-data - {:shapes [id1 id2 id3]})] + data (-> cp/default-page-data + (assoc-in [:objects uuid/zero :shapes] [id1 id2 id3]))] - (t/testing "mov-canvas 1" - (let [chg {:type :mov-shape - :id id3 - :index 0 - :session-id (uuid/next)} + (t/testing "abs order 1" + (let [chg {:type :mod-obj + :id uuid/zero + :operations [{:type :abs-order + :id id3 + :index 0}]} res (cp/process-changes data [chg])] - (t/is (= [id3 id1 id2] (:shapes res))))) - (t/testing "mov-canvas 2" - (let [chg {:type :mov-shape - :id id3 - :index 100 - :session-id (uuid/next)} - res (cp/process-changes data [chg])] - (t/is (= [id1 id2 id3] (:shapes res))))) + ;; (clojure.pprint/pprint data) + ;; (clojure.pprint/pprint res) - (t/testing "mov-canvas 3" - (let [chg {:type :mov-shape - :id id3 - :index 1 - :session-id (uuid/next)} + (t/is (= [id3 id1 id2] (get-in res [:objects uuid/zero :shapes]))))) + + (t/testing "abs order 2" + (let [chg {:type :mod-obj + :id uuid/zero + :operations [{:type :abs-order + :id id1 + :index 100}]} res (cp/process-changes data [chg])] - (t/is (= [id1 id3 id2] (:shapes res))))) + + ;; (clojure.pprint/pprint data) + ;; (clojure.pprint/pprint res) + + (t/is (= [id2 id3 id1] (get-in res [:objects uuid/zero :shapes]))))) + + (t/testing "abs order 3" + (let [chg {:type :mod-obj + :id uuid/zero + :operations [{:type :abs-order + :id id3 + :index 1}]} + res (cp/process-changes data [chg])] + + ;; (clojure.pprint/pprint data) + ;; (clojure.pprint/pprint res) + + (t/is (= [id1 id3 id2] (get-in res [:objects uuid/zero :shapes]))))) + )) + + +(t/deftest process-change-mod-obj-rel-order + (let [id1 (uuid/next) + id2 (uuid/next) + id3 (uuid/next) + data (-> cp/default-page-data + (assoc-in [:objects uuid/zero :shapes] [id1 id2 id3]))] + + (t/testing "rel order 1" + (let [chg {:type :mod-obj + :id uuid/zero + :operations [{:type :rel-order + :id id3 + :loc :down}]} + res (cp/process-changes data [chg])] + + ;; (clojure.pprint/pprint data) + ;; (clojure.pprint/pprint res) + + (t/is (= [id1 id3 id2] (get-in res [:objects uuid/zero :shapes]))))) + + + (t/testing "rel order 2" + (let [chg {:type :mod-obj + :id uuid/zero + :operations [{:type :rel-order + :id id1 + :loc :top}]} + res (cp/process-changes data [chg])] + + ;; (clojure.pprint/pprint data) + ;; (clojure.pprint/pprint res) + + (t/is (= [id2 id3 id1] (get-in res [:objects uuid/zero :shapes]))))) + + (t/testing "rel order 3" + (let [chg {:type :mod-obj + :id uuid/zero + :operations [{:type :rel-order + :id id2 + :loc :up}]} + res (cp/process-changes data [chg])] + + ;; (clojure.pprint/pprint data) + ;; (clojure.pprint/pprint res) + + (t/is (= [id1 id3 id2] (get-in res [:objects uuid/zero :shapes]))))) + + (t/testing "rel order 4" + (let [chg {:type :mod-obj + :id uuid/zero + :operations [{:type :rel-order + :id id3 + :loc :bottom}]} + res (cp/process-changes data [chg])] + + ;; (clojure.pprint/pprint data) + ;; (clojure.pprint/pprint res) + + (t/is (= [id3 id1 id2] (get-in res [:objects uuid/zero :shapes]))))) )) diff --git a/backend/tests/uxbox/tests/test_services_pages.clj b/backend/tests/uxbox/tests/test_services_pages.clj index 56c0a6fe84..73bf60e1b1 100644 --- a/backend/tests/uxbox/tests/test_services_pages.clj +++ b/backend/tests/uxbox/tests/test_services_pages.clj @@ -39,9 +39,20 @@ (t/is (= (:id data) (:id result))) (t/is (= (:name data) (:name result))) (t/is (= (:data data) (:data result))) + (t/is (nil? (:share-token result))) (t/is (= 0 (:version result))) (t/is (= 0 (:revn result)))))) + (t/testing "generate share token" + (let [data {::sm/type :generate-page-share-token + :id page-id} + out (th/try-on! (sm/handle data))] + + (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (string? (:share-token result)))))) + (t/testing "query pages" (let [data {::sq/type :pages :file-id (:id file) @@ -55,6 +66,7 @@ (t/is (= 1 (count result))) (t/is (= page-id (get-in result [0 :id]))) (t/is (= "test page" (get-in result [0 :name]))) + (t/is (string? (get-in result [0 :share-token]))) (t/is (:id file) (get-in result [0 :file-id]))))) (t/testing "delete page" diff --git a/backend/tests/uxbox/tests/test_services_viewer.clj b/backend/tests/uxbox/tests/test_services_viewer.clj new file mode 100644 index 0000000000..6ee84f23ba --- /dev/null +++ b/backend/tests/uxbox/tests/test_services_viewer.clj @@ -0,0 +1,102 @@ +(ns uxbox.tests.test-services-viewer + (:require + [clojure.test :as t] + [promesa.core :as p] + [datoteka.core :as fs] + [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.core :refer [system]] + [uxbox.http :as http] + [uxbox.services.mutations :as sm] + [uxbox.services.queries :as sq] + [uxbox.tests.helpers :as th] + [uxbox.util.storage :as ust] + [uxbox.util.uuid :as uuid] + [vertx.util :as vu])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(t/deftest retrieve-bundle + (let [prof @(th/create-profile db/pool 1) + prof2 @(th/create-profile db/pool 2) + team (:default-team prof) + proj (:default-project prof) + + file @(th/create-file db/pool (:id prof) (:id proj) 1) + page @(th/create-page db/pool (:id prof) (:id file) 1) + token (atom nil)] + + + (t/testing "authenticated with page-id" + (let [data {::sq/type :viewer-bundle + :profile-id (:id prof) + :page-id (:id page)} + + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (let [result (:result out)] + (t/is (contains? result :page)) + (t/is (contains? result :file)) + (t/is (contains? result :project))))) + + (t/testing "generate share token" + (let [data {::sm/type :generate-page-share-token + :id (:id page)} + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (string? (:share-token result))) + (reset! token (:share-token result))))) + + (t/testing "authenticated with page-id" + (let [data {::sq/type :viewer-bundle + :profile-id (:id prof2) + :page-id (:id page)} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :service-error)) + (t/is (= (:name error-data) :uxbox.services.queries.viewer/viewer-bundle))) + + (let [error (ex-cause (:error out)) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :not-found))))) + + (t/testing "authenticated with page-id and token" + (let [data {::sq/type :viewer-bundle + :profile-id (:id prof2) + :page-id (:id page) + :share-token @token} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + + (let [result (:result out)] + (t/is (contains? result :page)) + (t/is (contains? result :file)) + (t/is (contains? result :project))))) + + (t/testing "not authenticated with page-id and token" + (let [data {::sq/type :viewer-bundle + :page-id (:id page) + :share-token @token} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + + (let [result (:result out)] + (t/is (contains? result :page)) + (t/is (contains? result :file)) + (t/is (contains? result :project))))) + ))