diff --git a/backend/src/app/auth/ldap.clj b/backend/src/app/auth/ldap.clj index 7e2c30ce9c..5100abff9b 100644 --- a/backend/src/app/auth/ldap.clj +++ b/backend/src/app/auth/ldap.clj @@ -41,15 +41,18 @@ (reduce-kv clojure.string/replace s replacements)) (defn- search-user - [{:keys [conn attrs base-dn] :as cfg} email] - (let [query (replace-several (:query cfg) ":username" email) + [{:keys [::conn base-dn] :as cfg} email] + (let [query (replace-several (:query cfg) ":username" email) + attrs [(:attrs-username cfg) + (:attrs-email cfg) + (:attrs-fullname cfg)] params {:filter query :sizelimit 1 :attributes attrs}] (first (ldap/search conn base-dn params)))) (defn- retrieve-user - [{:keys [conn] :as cfg} {:keys [email password]}] + [{:keys [::conn] :as cfg} {:keys [email password]}] (when-let [{:keys [dn] :as user} (search-user cfg email)] (when (ldap/bind? conn dn password) {:fullname (get user (-> cfg :attrs-fullname keyword)) @@ -66,7 +69,7 @@ (defn authenticate [cfg params] (with-open [conn (connect cfg)] - (when-let [user (-> (assoc cfg :conn conn) + (when-let [user (-> (assoc cfg ::conn conn) (retrieve-user params))] (when-not (s/valid? ::info-data user) (let [explain (s/explain-str ::info-data user)] @@ -100,17 +103,6 @@ :host (:host cfg) :port (:port cfg) :cause cause) nil)))) -(defn- prepare-attributes - [cfg] - (assoc cfg :attrs [(:attrs-username cfg) - (:attrs-email cfg) - (:attrs-fullname cfg)])) - -(defmethod ig/init-key ::provider - [_ cfg] - (when (:enabled? cfg) - (some-> cfg try-connectivity prepare-attributes))) - (s/def ::enabled? ::us/boolean) (s/def ::host ::cf/ldap-host) (s/def ::port ::cf/ldap-port) @@ -124,8 +116,7 @@ (s/def ::attrs-fullname ::cf/ldap-attrs-fullname) (s/def ::attrs-username ::cf/ldap-attrs-username) -(defmethod ig/pre-init-spec ::provider - [_] +(s/def ::provider-params (s/keys :opt-un [::host ::port ::ssl ::tls ::enabled? @@ -135,3 +126,14 @@ ::attrs-email ::attrs-username ::attrs-fullname])) +(s/def ::provider + (s/nilable ::provider-params)) + +(defmethod ig/pre-init-spec ::provider + [_] + (s/spec ::provider)) + +(defmethod ig/init-key ::provider + [_ cfg] + (when (:enabled? cfg) + (try-connectivity cfg))) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index c333be29f9..8f7f1c7ea5 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -61,7 +61,7 @@ :public-uri "http://localhost:3449" :host "localhost" - :tenant "main" + :tenant "default" :redis-uri "redis://redis/0" :srepl-host "127.0.0.1" diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index d400d6387d..97664c280e 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -8,6 +8,7 @@ "Services related to the user activity (audit log)." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] @@ -20,6 +21,7 @@ [app.loggers.webhooks :as-alias webhooks] [app.main :as-alias main] [app.metrics :as mtx] + [app.rpc :as-alias rpc] [app.tokens :as tokens] [app.util.retry :as rtry] [app.util.time :as dt] @@ -171,18 +173,20 @@ (::webhooks/event? event)) (let [batch-key (::webhooks/batch-key event) batch-timeout (::webhooks/batch-timeout event) - label-suffix (when (ifn? batch-key) - (str/ffmt ":%" (batch-key (:props params)))) - dedupe? (boolean - (and batch-key batch-timeout))] + label (dm/str "rpc:" (:name params)) + label (cond + (ifn? batch-key) (dm/str label ":" (batch-key (::rpc/params event))) + (string? batch-key) (dm/str label ":" batch-key) + :else label) + dedupe? (boolean (and batch-key batch-timeout))] + (wrk/submit! ::wrk/conn pool ::wrk/task :process-webhook-event ::wrk/queue :webhooks ::wrk/max-retries 0 ::wrk/delay (or batch-timeout 0) ::wrk/dedupe dedupe? - ::wrk/label - (str/ffmt "rpc:%1%2" (:name params) label-suffix) + ::wrk/label label ::webhooks/event (-> params diff --git a/backend/src/app/loggers/database.clj b/backend/src/app/loggers/database.clj index 530dc8765f..f9a853f72f 100644 --- a/backend/src/app/loggers/database.clj +++ b/backend/src/app/loggers/database.clj @@ -11,12 +11,12 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] - [app.util.async :as aa] - [app.worker :as wrk] + [app.loggers.zmq :as lzmq] [clojure.core.async :as a] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [integrant.core :as ig])) + [integrant.core :as ig] + [promesa.exec :as px])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Error Listener @@ -27,7 +27,7 @@ (defonce enabled (atom true)) (defn- persist-on-database! - [{:keys [pool] :as cfg} {:keys [id] :as event}] + [{:keys [::db/pool] :as cfg} {:keys [id] :as event}] (when-not (db/read-only? pool) (db/insert! pool :server-error-report {:id id :content (db/tjson event)}))) @@ -53,41 +53,49 @@ (assoc :version (:full cf/version)) (update :id #(or % (uuid/next))))) -(defn handle-event - [{:keys [executor] :as cfg} event] - (aa/with-thread executor - (try - (let [event (parse-event event) - uri (cf/get :public-uri)] +(defn- handle-event + [cfg event] + (try + (let [event (parse-event event) + uri (cf/get :public-uri)] - (l/debug :hint "registering error on database" :id (:id event) - :uri (str uri "/dbg/error/" (:id event))) + (l/debug :hint "registering error on database" :id (:id event) + :uri (str uri "/dbg/error/" (:id event))) - (persist-on-database! cfg event)) - (catch Exception cause - (l/warn :hint "unexpected exception on database error logger" :cause cause))))) + (persist-on-database! cfg event)) + (catch Throwable cause + (l/warn :hint "unexpected exception on database error logger" :cause cause)))) -(defmethod ig/pre-init-spec ::reporter [_] - (s/keys :req-un [::wrk/executor ::db/pool ::receiver])) - -(defn error-event? +(defn- error-event? [event] (= "error" (:logger/level event))) +(defmethod ig/pre-init-spec ::reporter [_] + (s/keys :req [::db/pool ::lzmq/receiver])) + (defmethod ig/init-key ::reporter - [_ {:keys [receiver] :as cfg}] - (l/info :msg "initializing database error persistence") - (let [output (a/chan (a/sliding-buffer 5) (filter error-event?))] - (receiver :sub output) - (a/go-loop [] - (let [msg (a/ thread px/interrupt!)) diff --git a/backend/src/app/loggers/mattermost.clj b/backend/src/app/loggers/mattermost.clj index f7a1efb490..de89ba207c 100644 --- a/backend/src/app/loggers/mattermost.clj +++ b/backend/src/app/loggers/mattermost.clj @@ -38,13 +38,13 @@ (defn handle-event [cfg event] - (try - (let [event (ldb/parse-event event)] - (when @enabled - (send-mattermost-notification! cfg event))) - (catch Throwable cause - (l/warn :hint "unhandled error" - :cause cause)))) + (when @enabled + (try + (let [event (ldb/parse-event event)] + (send-mattermost-notification! cfg event)) + (catch Throwable cause + (l/warn :hint "unhandled error" + :cause cause))))) (defmethod ig/pre-init-spec ::reporter [_] (s/keys :req [::http/client diff --git a/backend/src/app/loggers/webhooks.clj b/backend/src/app/loggers/webhooks.clj index 89eda286d1..dc19fc3ae1 100644 --- a/backend/src/app/loggers/webhooks.clj +++ b/backend/src/app/loggers/webhooks.clj @@ -8,6 +8,7 @@ "A mattermost integration for error reporting." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.logging :as l] [app.common.transit :as t] [app.common.uri :as uri] @@ -21,6 +22,15 @@ [cuerdas.core :as str] [integrant.core :as ig])) +;; --- HELPERS + +(defn key-fn + [k & keys] + (fn [params] + (reduce #(dm/str %1 ":" (get params %2)) + (dm/str (get params k)) + keys))) + ;; --- PROC (defn- lookup-webhooks-by-team diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index b3d5431087..78250dce03 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -6,6 +6,7 @@ (ns app.main (:require + [app.auth.ldap :as-alias ldap] [app.auth.oidc :as-alias oidc] [app.auth.oidc.providers :as-alias oidc.providers] [app.common.logging :as l] @@ -231,7 +232,7 @@ :max-body-size (cf/get :http-server-max-body-size) :max-multipart-body-size (cf/get :http-server-max-multipart-body-size)} - :app.auth.ldap/provider + ::ldap/provider {:host (cf/get :ldap-host) :port (cf/get :ldap-port) :ssl (cf/get :ldap-ssl) @@ -327,6 +328,7 @@ ::db/pool (ig/ref ::db/pool) ::wrk/executor (ig/ref ::wrk/executor) ::props (ig/ref :app.setup/props) + ::ldap/provider (ig/ref ::ldap/provider) :pool (ig/ref ::db/pool) :session (ig/ref :app.http.session/manager) :sprops (ig/ref :app.setup/props) @@ -335,7 +337,6 @@ :msgbus (ig/ref :app.msgbus/msgbus) :public-uri (cf/get :public-uri) :redis (ig/ref ::rds/redis) - :ldap (ig/ref :app.auth.ldap/provider) :http-client (ig/ref ::http.client/client) :climit (ig/ref :app.rpc/climit) :rlimit (ig/ref :app.rpc/rlimit) @@ -450,9 +451,8 @@ ::http.client/client (ig/ref ::http.client/client)} :app.loggers.database/reporter - {:receiver (ig/ref :app.loggers.zmq/receiver) - :pool (ig/ref ::db/pool) - :executor (ig/ref ::wrk/executor)} + {::lzmq/receiver (ig/ref :app.loggers.zmq/receiver) + ::db/pool (ig/ref ::db/pool)} ::sto/storage {:pool (ig/ref ::db/pool) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 26e39ef09c..6681c13b1f 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -6,6 +6,7 @@ (ns app.rpc (:require + [app.auth.ldap :as-alias ldap] [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] @@ -72,12 +73,14 @@ internal async flow into ring async flow." [methods {:keys [profile-id session-id params] :as request} respond raise] (let [type (keyword (:type params)) - data (into {::http/request request} params) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::http/request request)) data (if profile-id - (assoc data - :profile-id profile-id - ::profile-id profile-id - ::session-id session-id) + (-> data + (assoc :profile-id profile-id) + (assoc ::profile-id profile-id) + (assoc ::session-id session-id)) (dissoc data :profile-id ::profile-id)) method (get methods type default-handler)] @@ -93,14 +96,15 @@ internal async flow into ring async flow." [methods {:keys [profile-id session-id params] :as request} respond raise] (let [type (keyword (:type params)) - data (into {::http/request request} params) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::http/request request)) data (if profile-id - (assoc data - :profile-id profile-id - ::profile-id profile-id - ::session-id session-id) + (-> data + (assoc :profile-id profile-id) + (assoc ::profile-id profile-id) + (assoc ::session-id session-id)) (dissoc data :profile-id ::profile-id)) - method (get methods type default-handler)] (-> (method data) (p/then (partial handle-response request)) @@ -115,12 +119,15 @@ [methods {:keys [profile-id session-id params] :as request} respond raise] (let [cmd (keyword (:type params)) etag (yrq/get-header request "if-none-match") - data (into {::request-at (dt/now) - ::http/request request - ::cond/key etag} params) - data (if profile-id - (assoc data ::profile-id profile-id ::session-id session-id) - (dissoc data ::profile-id)) + + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::http/request request) + (assoc ::cond/key etag) + (cond-> (uuid? profile-id) + (-> (assoc ::profile-id profile-id) + (assoc ::session-id session-id)))) + method (get methods cmd default-handler)] (binding [cond/*enabled* true] (-> (method data) @@ -184,6 +191,12 @@ :profile-id profile-id :ip-addr (some-> request audit/parse-client-ip) :props props + + ;; NOTE: for batch-key lookup we need the params as-is + ;; because the rpc api does not need to know the + ;; audit/webhook specific object layout. + ::params (dissoc params ::http/request) + ::webhooks/batch-key (or (::webhooks/batch-key mdata) (::webhooks/batch-key resultm)) @@ -281,6 +294,7 @@ 'app.rpc.commands.management 'app.rpc.commands.verify-token 'app.rpc.commands.search + 'app.rpc.commands.media 'app.rpc.commands.teams 'app.rpc.commands.auth 'app.rpc.commands.ldap @@ -306,6 +320,7 @@ (s/keys :req [::audit/collector ::http.client/client ::db/pool + ::ldap/provider ::wrk/executor] :req-un [::sto/storage ::http.session/session @@ -316,8 +331,7 @@ ::climit ::wrk/executor ::mtx/metrics - ::db/pool - ::ldap])) + ::db/pool])) (defmethod ig/init-key ::methods [_ cfg] diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index 44efcd7f47..b12e12b22b 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -72,7 +72,7 @@ (sv/defmethod ::push-audit-events {::climit/queue :push-audit-events - ::climit/key-fn :profile-id + ::climit/key-fn ::rpc/profile-id ::audit/skip true ::doc/added "1.17"} [{:keys [::db/pool ::wrk/executor] :as cfg} params] diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index 168c5a295c..73e40642f6 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -288,7 +288,9 @@ (sv/defmethod ::create-comment-thread {::doc/added "1.15" ::webhooks/event? true} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}] + [{:keys [::db/pool] :as cfg} + {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}] + (db/with-atomic [conn pool] (let [{:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)] (files/check-comment-permissions! conn profile-id file-id share-id) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index affa92e572..059d3aa949 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -268,7 +268,7 @@ {::doc/added "1.17" ::cond/get-object #(get-minimal-file %1 (:id %2)) ::cond/key-fn get-file-etag} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features]}] (with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id id)] (check-read-permissions! perms) @@ -296,7 +296,7 @@ "Retrieve a file by its ID. Only authenticated users." {::doc/added "1.17" ::rpc/:auth false} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] }] (with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id file-id share-id)] (check-read-permissions! perms) @@ -363,7 +363,7 @@ (sv/defmethod ::get-project-files "Get all files for the specified project." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id]}] (with-open [conn (db/open pool)] (projects/check-read-permissions! conn profile-id project-id) (get-project-files conn project-id))) @@ -376,15 +376,16 @@ (s/def ::file-id ::us/uuid) (s/def ::has-file-libraries - (s/keys :req [::rpc/profile-id] :req-un [::file-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::file-id])) (sv/defmethod ::has-file-libraries "Checks if the file has libraries. Returns a boolean" {::doc/added "1.15.1"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id]}] (with-open [conn (db/open pool)] (check-read-permissions! pool profile-id file-id) - (get-has-file-libraries conn params))) + (get-has-file-libraries conn file-id))) (def ^:private sql:has-file-libraries "SELECT COUNT(*) > 0 AS has_libraries @@ -395,7 +396,7 @@ fl.deleted_at > now())") (defn- get-has-file-libraries - [conn {:keys [file-id]}] + [conn file-id] (let [row (db/exec-one! conn [sql:has-file-libraries file-id])] (:has-libraries row))) @@ -474,7 +475,7 @@ order by f.modified_at desc") (defn get-team-shared-files - [conn {:keys [team-id] :as params}] + [conn team-id] (letfn [(assets-sample [assets limit] (let [sorted-assets (->> (vals assets) (sort-by #(str/lower (:name %))))] @@ -494,14 +495,16 @@ (map #(dissoc % :data))))))) (s/def ::get-team-shared-files - (s/keys :req [::rpc/profile-id] :req-un [::team-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id])) (sv/defmethod ::get-team-shared-files "Get all file (libraries) for the specified team." {::doc/added "1.17"} - [{:keys [pool] :as cfg} params] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (with-open [conn (db/open pool)] - (get-team-shared-files conn params))) + (teams/check-read-permissions! conn profile-id team-id) + (get-team-shared-files conn team-id))) ;; --- COMMAND QUERY: get-file-libraries @@ -552,7 +555,7 @@ (sv/defmethod ::get-file-libraries "Get libraries used by the specified file." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features]}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (get-file-libraries conn file-id features))) @@ -583,7 +586,6 @@ (check-read-permissions! conn profile-id file-id) (get-library-file-references conn file-id))) - ;; --- COMMAND QUERY: get-team-recent-files (def sql:team-recent-files @@ -765,7 +767,7 @@ ;; --- MUTATION COMMAND: rename-file (defn rename-file - [conn {:keys [id name] :as params}] + [conn {:keys [id name]}] (db/update! conn :file {:name name :modified-at (dt/now)} @@ -899,7 +901,7 @@ ;; --- MUTATION COMMAND: unlink-file-from-library (defn unlink-file-from-library - [conn {:keys [file-id library-id] :as params}] + [conn {:keys [file-id library-id]}] (db/delete! conn :file-library-rel {:file-id file-id :library-file-id library-id})) diff --git a/backend/src/app/rpc/commands/files/update.clj b/backend/src/app/rpc/commands/files/update.clj index f24dede18f..79cf1d7592 100644 --- a/backend/src/app/rpc/commands/files/update.clj +++ b/backend/src/app/rpc/commands/files/update.clj @@ -17,7 +17,7 @@ [app.config :as cf] [app.db :as db] [app.loggers.audit :as audit] - [app.loggers.webhooks :as-alias webhooks] + [app.loggers.webhooks :as webhooks] [app.metrics :as mtx] [app.msgbus :as mbus] [app.rpc :as-alias rpc] @@ -130,7 +130,7 @@ ::climit/key-fn :id ::webhooks/event? true ::webhooks/batch-timeout (dt/duration "2m") - ::webhooks/batch-key :id + ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id) ::doc/added "1.17"} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index 485194f6c4..6283e14231 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -12,10 +12,13 @@ [app.db :as db] [app.http.session :as session] [app.loggers.audit :as-alias audit] + [app.main :as-alias main] + [app.rpc :as-alias rpc] [app.rpc.commands.auth :as cmd.auth] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.queries.profile :as profile] + [app.tokens :as tokens] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -34,15 +37,15 @@ (sv/defmethod ::login-with-ldap "Performs the authentication using LDAP backend. Only works if LDAP is properly configured and enabled with `login-with-ldap` flag." - {:auth false + {::rpc/auth false ::doc/added "1.15"} - [{:keys [session tokens ldap] :as cfg} params] - (when-not ldap + [{:keys [::main/props ::ldap/provider session] :as cfg} params] + (when-not provider (ex/raise :type :restriction :code :ldap-not-initialized :hide "ldap auth provider is not initialized")) - (let [info (ldap/authenticate ldap params)] + (let [info (ldap/authenticate provider params)] (when-not info (ex/raise :type :validation :code :wrong-credentials)) @@ -58,12 +61,11 @@ ;; user comes from team-invitation process; in this case, ;; regenerate token and send back to the user a new invitation ;; token (and mark current session as logged). - (let [claims (tokens :verify {:token token :iss :team-invitation}) + (let [claims (tokens/verify props {:token token :iss :team-invitation}) claims (assoc claims :member-id (:id profile) :member-email (:email profile)) - token (tokens :generate claims)] - + token (tokens/generate props claims)] (-> {:invitation-token token} (rph/with-transform (session/create-fn session (:id profile))) (rph/with-meta {::audit/props (:props profile) diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index f3f882ef5d..2337ff844c 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -46,9 +46,9 @@ "Duplicate a single file in the same team." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} params] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] - (duplicate-file conn (assoc params :profile-id (::rpc/profile-id params))))) + (duplicate-file conn (assoc params :profile-id profile-id)))) (defn- remap-id [item index key] @@ -136,7 +136,7 @@ and so.deleted_at is null") (defn duplicate-file* - [conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag] :as opts}] + [conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag]}] (let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)])) fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)])) @@ -329,10 +329,9 @@ "Move a set of files from one project to other." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} params] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] - (move-files conn (assoc params :profile-id (::rpc/profile-id params))))) - + (move-files conn (assoc params :profile-id profile-id)))) ;; --- COMMAND: Move project @@ -370,9 +369,9 @@ "Move projects between teams." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} params] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] - (move-project conn (assoc params :profile-id (::rpc/profile-id params))))) + (move-project conn (assoc params :profile-id profile-id)))) ;; --- COMMAND: Clone Template @@ -387,10 +386,10 @@ "Clone into the specified project the template by its id." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} params] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (-> (assoc cfg :conn conn) - (clone-template (assoc params :profile-id (::rpc/profile-id params)))))) + (clone-template (assoc params :profile-id profile-id))))) (defn- clone-template [{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}] diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj new file mode 100644 index 0000000000..e22a7bb859 --- /dev/null +++ b/backend/src/app/rpc/commands/media.clj @@ -0,0 +1,274 @@ +;; 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.media + (:require + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.media :as cm] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.http.client :as http] + [app.media :as media] + [app.rpc :as-alias rpc] + [app.rpc.climit :as climit] + [app.rpc.commands.files :as files] + [app.rpc.doc :as-alias doc] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [app.util.services :as sv] + [app.util.time :as dt] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [datoteka.io :as io] + [promesa.core :as p] + [promesa.exec :as px])) + +(def default-max-file-size + (* 1024 1024 10)) ; 10 MiB + +(def thumbnail-options + {:width 100 + :height 100 + :quality 85 + :format :jpeg}) + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::file-id ::us/uuid) +(s/def ::team-id ::us/uuid) + +(defn validate-content-size! + [content] + (when (> (:size content) (cf/get :media-max-file-size default-max-file-size)) + (ex/raise :type :restriction + :code :media-max-file-size-reached + :hint (str/ffmt "the uploaded file size % is greater than the maximum %" + (:size content) + default-max-file-size)))) + +;; --- Create File Media object (upload) + +(declare create-file-media-object) + +(s/def ::content ::media/upload) +(s/def ::is-local ::us/boolean) + +(s/def ::upload-file-media-object + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::is-local ::name ::content] + :opt-un [::id])) + +(sv/defmethod ::upload-file-media-object + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}] + (let [cfg (update cfg :storage media/configure-assets-storage)] + (files/check-edition-permissions! pool profile-id file-id) + (media/validate-media-type! content) + (validate-content-size! content) + + (create-file-media-object cfg params))) + +(defn- big-enough-for-thumbnail? + "Checks if the provided image info is big enough for + create a separate thumbnail storage object." + [info] + (or (> (:width info) (:width thumbnail-options)) + (> (:height info) (:height thumbnail-options)))) + +(defn- svg-image? + [info] + (= (:mtype info) "image/svg+xml")) + +;; NOTE: we use the `on conflict do update` instead of `do nothing` +;; because postgresql does not returns anything if no update is +;; performed, the `do update` does the trick. + +(def sql:create-file-media-object + "insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype) + values (?, ?, ?, ?, ?, ?, ?, ?, ?) + on conflict (id) do update set created_at=file_media_object.created_at + returning *") + +;; NOTE: the following function executes without a transaction, this +;; means that if something fails in the middle of this function, it +;; will probably leave leaked/unreferenced objects in the database and +;; probably in the storage layer. For handle possible object leakage, +;; we create all media objects marked as touched, this ensures that if +;; something fails, all leaked (already created storage objects) will +;; be eventually marked as deleted by the touched-gc task. +;; +;; The touched-gc task, performs periodic analysis of all touched +;; storage objects and check references of it. This is the reason why +;; `reference` metadata exists: it indicates the name of the table +;; witch holds the reference to storage object (it some kind of +;; inverse, soft referential integrity). + +(defn create-file-media-object + [{:keys [storage pool climit executor]} + {:keys [id file-id is-local name content]}] + (letfn [;; Function responsible to retrieve the file information, as + ;; it is synchronous operation it should be wrapped into + ;; with-dispatch macro. + (get-info [content] + (climit/with-dispatch (:process-image climit) + (media/run {:cmd :info :input content}))) + + ;; Function responsible of calculating cryptographyc hash of + ;; the provided data. + (calculate-hash [data] + (px/with-dispatch executor + (sto/calculate-hash data))) + + ;; Function responsible of generating thumnail. As it is synchronous + ;; opetation, it should be wrapped into with-dispatch macro + (generate-thumbnail [info] + (climit/with-dispatch (:process-image climit) + (media/run (assoc thumbnail-options + :cmd :generic-thumbnail + :input info)))) + + (create-thumbnail [info] + (when (and (not (svg-image? info)) + (big-enough-for-thumbnail? info)) + (p/let [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 + ::sto/touched-at (dt/now) + :content-type (:mtype thumb) + :bucket "file-media-object"})))) + + (create-image [info] + (p/let [data (:path info) + hash (calculate-hash data) + content (-> (sto/content data) + (sto/wrap-with-hash hash))] + (sto/put-object! storage + {::sto/content content + ::sto/deduplicate? true + ::sto/touched-at (dt/now) + :content-type (:mtype info) + :bucket "file-media-object"}))) + + (insert-into-database [info image thumb] + (px/with-dispatch executor + (db/exec-one! pool [sql:create-file-media-object + (or id (uuid/next)) + file-id is-local name + (:id image) + (:id thumb) + (:width info) + (:height info) + (:mtype info)])))] + + (p/let [info (get-info content) + thumb (create-thumbnail info) + image (create-image info)] + (insert-into-database info image thumb)))) + +;; --- Create File Media Object (from URL) + +(declare ^:private create-file-media-object-from-url) + +(s/def ::create-file-media-object-from-url + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::is-local ::url] + :opt-un [::id ::name])) + +(sv/defmethod ::create-file-media-object-from-url + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + (let [cfg (update cfg :storage media/configure-assets-storage)] + (files/check-edition-permissions! pool profile-id file-id) + (create-file-media-object-from-url cfg params))) + +(defn- create-file-media-object-from-url + [cfg {:keys [url name] :as params}] + (letfn [(parse-and-validate-size [headers] + (let [size (some-> (get headers "content-length") d/parse-integer) + mtype (get headers "content-type") + format (cm/mtype->format mtype) + max-size (cf/get :media-max-file-size default-max-file-size)] + + (when-not size + (ex/raise :type :validation + :code :unknown-size + :hint "seems like the url points to resource with unknown size")) + + (when (> size max-size) + (ex/raise :type :validation + :code :file-too-large + :hint (str/ffmt "the file size % is greater than the maximum %" + size + default-max-file-size))) + + (when (nil? format) + (ex/raise :type :validation + :code :media-type-not-allowed + :hint "seems like the url points to an invalid media object")) + + {:size size + :mtype mtype + :format format})) + + (download-media [uri] + (-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream}) + (p/then process-response))) + + (process-response [{:keys [body headers] :as response}] + (let [{:keys [size mtype]} (parse-and-validate-size headers) + path (tmp/tempfile :prefix "penpot.media.download.") + written (io/write-to-file! body path :size size)] + + (when (not= written size) + (ex/raise :type :internal + :code :mismatch-write-size + :hint "unexpected state: unable to write to file")) + + {:filename "tempfile" + :size size + :path path + :mtype mtype}))] + + (p/let [content (download-media url)] + (->> (merge params {:content content :name (or name (:filename content))}) + (create-file-media-object cfg))))) + +;; --- Clone File Media object (Upload and create from url) + +(declare clone-file-media-object) + +(s/def ::clone-file-media-object + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::is-local ::id])) + +(sv/defmethod ::clone-file-media-object + {::doc/added "1.17"} + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + (db/with-atomic [conn pool] + (files/check-edition-permissions! conn profile-id file-id) + (-> (assoc cfg :conn conn) + (clone-file-media-object params)))) + +(defn clone-file-media-object + [{:keys [conn]} {:keys [id file-id is-local]}] + (let [mobj (db/get-by-id conn :file-media-object id)] + (db/insert! conn :file-media-object + {:id (uuid/next) + :file-id file-id + :is-local is-local + :name (:name mobj) + :media-id (:media-id mobj) + :thumbnail-id (:thumbnail-id mobj) + :width (:width mobj) + :height (:height mobj) + :mtype (:mtype mobj)}))) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 876b609df4..141664b8c6 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -34,10 +34,10 @@ {::climit/queue :auth ::climit/key-fn ::rpc/profile-id ::doc/added "1.18"} - [{:keys [::db/pool]} {:keys [password] :as params}] + [{:keys [::db/pool]} {:keys [::rpc/profile-id password]}] (db/with-atomic [conn pool] (let [admins (cf/get :admins) - profile (db/get-by-id conn :profile (::rpc/profile-id params))] + profile (db/get-by-id conn :profile profile-id)] (if (or (:is-admin profile) (contains? admins (:email profile))) diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj index 92534fafcf..5f1ddc559f 100644 --- a/backend/src/app/rpc/commands/search.clj +++ b/backend/src/app/rpc/commands/search.clj @@ -48,7 +48,7 @@ order by f.created_at asc") (defn search-files - [conn {:keys [::rpc/profile-id team-id search-term] :as params}] + [conn profile-id team-id search-term] (db/exec! conn [sql:search-files profile-id team-id profile-id team-id @@ -64,6 +64,5 @@ (sv/defmethod ::search-files {::doc/added "1.17"} - [{:keys [pool]} {:keys [search-term] :as params}] - (when search-term - (search-files pool params))) + [{:keys [pool]} {:keys [::rpc/profile-id team-id search-term]}] + (some->> search-term (search-files pool profile-id team-id))) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index f040a034f9..bf2346f2f7 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -385,14 +385,8 @@ (declare role->params) -(s/def ::reassign-to ::us/uuid) -(s/def ::leave-team - (s/keys :req [::rpc/profile-id] - :req-un [::id] - :opt-un [::reassign-to])) - (defn leave-team - [conn {:keys [::rpc/profile-id id reassign-to]}] + [conn {:keys [profile-id id reassign-to]}] (let [perms (get-permissions conn profile-id id) members (retrieve-team-members conn id)] @@ -437,12 +431,17 @@ nil)) +(s/def ::reassign-to ::us/uuid) +(s/def ::leave-team + (s/keys :req [::rpc/profile-id] + :req-un [::id] + :opt-un [::reassign-to])) (sv/defmethod ::leave-team {::doc/added "1.17"} - [{:keys [pool] :as cfg} params] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] - (leave-team conn params))) + (leave-team conn (assoc params :profile-id profile-id)))) ;; --- Mutation: Delete Team @@ -539,9 +538,9 @@ (sv/defmethod ::update-team-member-role {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} params] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] - (update-team-member-role conn (assoc params :profile-id (::rpc/profile-id params))))) + (update-team-member-role conn (assoc params :profile-id profile-id)))) ;; --- Mutation: Delete Team Member diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index b68dc7e531..c19e84824f 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -84,6 +84,6 @@ ::cond/key-fn files/get-file-etag ::cond/reuse-key? true ::doc/added "1.17"} - [{:keys [pool]} params] + [{:keys [pool]} {:keys [::rpc/profile-id] :as params}] (with-open [conn (db/open pool)] - (get-view-only-bundle conn (assoc params :profile-id (::rpc/profile-id params))))) + (get-view-only-bundle conn (assoc params :profile-id profile-id)))) diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index dba890ed43..f667395490 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -6,280 +6,49 @@ (ns app.rpc.mutations.media (:require - [app.common.data :as d] - [app.common.exceptions :as ex] - [app.common.media :as cm] - [app.common.spec :as us] - [app.common.uuid :as uuid] - [app.config :as cf] [app.db :as db] - [app.http.client :as http] [app.media :as media] - [app.rpc.climit :as climit] - [app.rpc.commands.teams :as teams] - [app.storage :as sto] - [app.storage.tmp :as tmp] + [app.rpc.commands.files :as files] + [app.rpc.commands.media :as cmd.media] + [app.rpc.doc :as-alias doc] [app.util.services :as sv] - [app.util.time :as dt] - [clojure.spec.alpha :as s] - [cuerdas.core :as str] - [datoteka.io :as io] - [promesa.core :as p] - [promesa.exec :as px])) - -(def default-max-file-size (* 1024 1024 10)) ; 10 MiB - -(def thumbnail-options - {:width 100 - :height 100 - :quality 85 - :format :jpeg}) - -(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) + [clojure.spec.alpha :as s])) ;; --- Create File Media object (upload) -(declare create-file-media-object) -(declare select-file) - -(s/def ::content ::media/upload) -(s/def ::is-local ::us/boolean) - -(s/def ::upload-file-media-object - (s/keys :req-un [::profile-id ::file-id ::is-local ::name ::content] - :opt-un [::id])) +(s/def ::upload-file-media-object ::cmd.media/upload-file-media-object) (sv/defmethod ::upload-file-media-object + {::doc/added "1.2" + ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}] - (let [file (select-file pool file-id) - cfg (update cfg :storage media/configure-assets-storage)] - - (teams/check-edition-permissions! pool profile-id (:team-id file)) + (let [cfg (update cfg :storage media/configure-assets-storage)] + (files/check-edition-permissions! pool profile-id file-id) (media/validate-media-type! content) - - (when (> (:size content) (cf/get :media-max-file-size default-max-file-size)) - (ex/raise :type :restriction - :code :media-max-file-size-reached - :hint (str/ffmt "the uploaded file size % is greater than the maximum %" - (:size content) - default-max-file-size))) - - (create-file-media-object cfg params))) - -(defn- big-enough-for-thumbnail? - "Checks if the provided image info is big enough for - create a separate thumbnail storage object." - [info] - (or (> (:width info) (:width thumbnail-options)) - (> (:height info) (:height thumbnail-options)))) - -(defn- svg-image? - [info] - (= (:mtype info) "image/svg+xml")) - -;; NOTE: we use the `on conflict do update` instead of `do nothing` -;; because postgresql does not returns anything if no update is -;; performed, the `do update` does the trick. - -(def sql:create-file-media-object - "insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype) - values (?, ?, ?, ?, ?, ?, ?, ?, ?) - on conflict (id) do update set created_at=file_media_object.created_at - returning *") - -;; NOTE: the following function executes without a transaction, this -;; means that if something fails in the middle of this function, it -;; will probably leave leaked/unreferenced objects in the database and -;; probably in the storage layer. For handle possible object leakage, -;; we create all media objects marked as touched, this ensures that if -;; something fails, all leaked (already created storage objects) will -;; be eventually marked as deleted by the touched-gc task. -;; -;; The touched-gc task, performs periodic analysis of all touched -;; storage objects and check references of it. This is the reason why -;; `reference` metadata exists: it indicates the name of the table -;; witch holds the reference to storage object (it some kind of -;; inverse, soft referential integrity). - -(defn create-file-media-object - [{:keys [storage pool climit executor] :as cfg} - {:keys [id file-id is-local name content] :as params}] - (letfn [;; Function responsible to retrieve the file information, as - ;; it is synchronous operation it should be wrapped into - ;; with-dispatch macro. - (get-info [content] - (climit/with-dispatch (:process-image climit) - (media/run {:cmd :info :input content}))) - - ;; Function responsible of calculating cryptographyc hash of - ;; the provided data. - (calculate-hash [data] - (px/with-dispatch executor - (sto/calculate-hash data))) - - ;; Function responsible of generating thumnail. As it is synchronous - ;; opetation, it should be wrapped into with-dispatch macro - (generate-thumbnail [info] - (climit/with-dispatch (:process-image climit) - (media/run (assoc thumbnail-options - :cmd :generic-thumbnail - :input info)))) - - (create-thumbnail [info] - (when (and (not (svg-image? info)) - (big-enough-for-thumbnail? info)) - (p/let [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 - ::sto/touched-at (dt/now) - :content-type (:mtype thumb) - :bucket "file-media-object"})))) - - (create-image [info] - (p/let [data (:path info) - hash (calculate-hash data) - content (-> (sto/content data) - (sto/wrap-with-hash hash))] - (sto/put-object! storage - {::sto/content content - ::sto/deduplicate? true - ::sto/touched-at (dt/now) - :content-type (:mtype info) - :bucket "file-media-object"}))) - - (insert-into-database [info image thumb] - (px/with-dispatch executor - (db/exec-one! pool [sql:create-file-media-object - (or id (uuid/next)) - file-id is-local name - (:id image) - (:id thumb) - (:width info) - (:height info) - (:mtype info)])))] - - (p/let [info (get-info content) - thumb (create-thumbnail info) - image (create-image info)] - (insert-into-database info image thumb)))) + (cmd.media/validate-content-size! content) + (cmd.media/create-file-media-object cfg params))) ;; --- Create File Media Object (from URL) -(declare ^:private create-file-media-object-from-url) - -(s/def ::create-file-media-object-from-url - (s/keys :req-un [::profile-id ::file-id ::is-local ::url] - :opt-un [::id ::name])) +(s/def ::create-file-media-object-from-url ::cmd.media/create-file-media-object-from-url) (sv/defmethod ::create-file-media-object-from-url + {::doc/added "1.3" + ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (let [file (select-file pool file-id) - cfg (update cfg :storage media/configure-assets-storage)] - (teams/check-edition-permissions! pool profile-id (:team-id file)) - (create-file-media-object-from-url cfg params))) - -(defn- create-file-media-object-from-url - [cfg {:keys [url name] :as params}] - (letfn [(parse-and-validate-size [headers] - (let [size (some-> (get headers "content-length") d/parse-integer) - mtype (get headers "content-type") - format (cm/mtype->format mtype) - max-size (cf/get :media-max-file-size default-max-file-size)] - - (when-not size - (ex/raise :type :validation - :code :unknown-size - :hint "seems like the url points to resource with unknown size")) - - (when (> size max-size) - (ex/raise :type :validation - :code :file-too-large - :hint (str/ffmt "the file size % is greater than the maximum %" - size - default-max-file-size))) - - (when (nil? format) - (ex/raise :type :validation - :code :media-type-not-allowed - :hint "seems like the url points to an invalid media object")) - - {:size size - :mtype mtype - :format format})) - - (download-media [uri] - (-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream}) - (p/then process-response))) - - (process-response [{:keys [body headers] :as response}] - (let [{:keys [size mtype]} (parse-and-validate-size headers) - path (tmp/tempfile :prefix "penpot.media.download.") - written (io/write-to-file! body path :size size)] - - (when (not= written size) - (ex/raise :type :internal - :code :mismatch-write-size - :hint "unexpected state: unable to write to file")) - - {:filename "tempfile" - :size size - :path path - :mtype mtype}))] - - (p/let [content (download-media url)] - (->> (merge params {:content content :name (or name (:filename content))}) - (create-file-media-object cfg))))) + (let [cfg (update cfg :storage media/configure-assets-storage)] + (files/check-edition-permissions! pool profile-id file-id) + (#'cmd.media/create-file-media-object-from-url cfg params))) ;; --- Clone File Media object (Upload and create from url) -(declare clone-file-media-object) - -(s/def ::clone-file-media-object - (s/keys :req-un [::profile-id ::file-id ::is-local ::id])) +(s/def ::clone-file-media-object ::cmd.media/clone-file-media-object) (sv/defmethod ::clone-file-media-object + {::doc/added "1.2" + ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] - (let [file (select-file conn file-id)] - (teams/check-edition-permissions! conn profile-id (:team-id file)) - (-> (assoc cfg :conn conn) - (clone-file-media-object params))))) - -(defn clone-file-media-object - [{:keys [conn] :as cfg} {:keys [id file-id is-local]}] - (let [mobj (db/get-by-id conn :file-media-object id)] - (db/insert! conn :file-media-object - {:id (uuid/next) - :file-id file-id - :is-local is-local - :name (:name mobj) - :media-id (:media-id mobj) - :thumbnail-id (:thumbnail-id mobj) - :width (:width mobj) - :height (:height mobj) - :mtype (:mtype mobj)}))) - -;; --- HELPERS - -(def ^:private - sql:select-file - "select file.*, - project.team_id as team_id - from file - inner join project on (project.id = file.project_id) - where file.id = ?") - -(defn- select-file - [conn id] - (let [row (db/exec-one! conn [sql:select-file id])] - (when-not row - (ex/raise :type :not-found)) - row)) + (files/check-edition-permissions! conn profile-id file-id) + (-> (assoc cfg :conn conn) + (cmd.media/clone-file-media-object params)))) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj index 398b1400ad..3dc5d9f926 100644 --- a/backend/src/app/rpc/queries/files.clj +++ b/backend/src/app/rpc/queries/files.clj @@ -179,6 +179,5 @@ (sv/defmethod ::search-files {::doc/added "1.0" ::doc/deprecated "1.17"} - [{:keys [pool]} {:keys [search-term] :as params}] - (when search-term - (search/search-files pool params))) + [{:keys [pool]} {:keys [profile-id team-id search-term]}] + (some->> search-term (search/search-files pool profile-id team-id))) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 068aa5a513..7e906dfbba 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -6,13 +6,14 @@ (ns backend-tests.rpc-file-test (:require - [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.db :as db] [app.db.sql :as sql] [app.http :as http] + [app.rpc :as-alias rpc] [app.storage :as sto] [app.util.time :as dt] + [backend-tests.helpers :as th] [clojure.test :as t] [datoteka.core :as fs])) @@ -28,13 +29,13 @@ (t/testing "create file" (let [data {::th/type :create-file - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :project-id proj-id :id file-id :name "foobar" :is-shared false :components-v2 true} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -47,8 +48,8 @@ (let [data {::th/type :rename-file :id file-id :name "new name" - :profile-id (:id prof)} - out (th/mutation! data)] + ::rpc/profile-id (:id prof)} + out (th/command! data)] ;; (th/print-result! out) (let [result (:result out)] @@ -56,10 +57,10 @@ (t/is (= (:name data) (:name result)))))) (t/testing "query files" - (let [data {::th/type :project-files - :project-id proj-id - :profile-id (:id prof)} - out (th/query! data)] + (let [data {::th/type :get-project-files + ::rpc/profile-id (:id prof) + :project-id proj-id} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -70,11 +71,11 @@ (t/is (= "new name" (get-in result [0 :name])))))) (t/testing "query single file without users" - (let [data {::th/type :file - :profile-id (:id prof) + (let [data {::th/type :get-file + ::rpc/profile-id (:id prof) :id file-id :components-v2 true} - out (th/query! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -88,18 +89,18 @@ (t/testing "delete file" (let [data {::th/type :delete-file :id file-id - :profile-id (:id prof)} - out (th/mutation! data)] + ::rpc/profile-id (:id prof)} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (nil? (:result out))))) (t/testing "query single file after delete" - (let [data {::th/type :file - :profile-id (:id prof) + (let [data {::th/type :get-file + ::rpc/profile-id (:id prof) :id file-id :components-v2 true} - out (th/query! data)] + out (th/command! data)] ;; (th/print-result! out) @@ -109,10 +110,10 @@ (t/is (= (:type error-data) :not-found))))) (t/testing "query list files after delete" - (let [data {::th/type :project-files - :project-id proj-id - :profile-id (:id prof)} - out (th/query! data)] + (let [data {::th/type :get-project-files + ::rpc/profile-id (:id prof) + :project-id proj-id} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -136,19 +137,18 @@ out (th/mutation! params)] ;; (th/print-result! out) - (t/is (nil? (:error out))) (:result out))) (update-file [{:keys [profile-id file-id changes revn] :or {revn 0}}] (let [params {::th/type :update-file + ::rpc/profile-id profile-id :id file-id :session-id (uuid/random) - :profile-id profile-id :revn revn :components-v2 true :changes changes} - out (th/mutation! params)] + out (th/command! params)] (t/is (nil? (:error out))) (:result out)))] @@ -257,12 +257,12 @@ profile2 (th/create-profile* 2) data {::th/type :create-file - :profile-id (:id profile2) + ::rpc/profile-id (:id profile2) :project-id (:default-project-id profile1) :name "foobar" :is-shared false :components-v2 true} - out (th/mutation! data) + out (th/command! data) error (:error out)] ;; (th/print-result! out) @@ -277,9 +277,9 @@ :profile-id (:id profile1)}) data {::th/type :rename-file :id (:id file) - :profile-id (:id profile2) + ::rpc/profile-id (:id profile2) :name "foobar"} - out (th/mutation! data) + out (th/command! data) error (:error out)] ;; (th/print-result! out) @@ -293,9 +293,9 @@ file (th/create-file* 1 {:project-id (:default-project-id profile1) :profile-id (:id profile1)}) data {::th/type :delete-file - :profile-id (:id profile2) + ::rpc/profile-id (:id profile2) :id (:id file)} - out (th/mutation! data) + out (th/command! data) error (:error out)] ;; (th/print-result! out) @@ -308,10 +308,10 @@ file (th/create-file* 1 {:project-id (:default-project-id profile1) :profile-id (:id profile1)}) data {::th/type :set-file-shared - :profile-id (:id profile2) + ::rpc/profile-id (:id profile2) :id (:id file) :is-shared true} - out (th/mutation! data) + out (th/command! data) error (:error out)] ;; (th/print-result! out) @@ -328,11 +328,11 @@ :profile-id (:id profile1)}) data {::th/type :link-file-to-library - :profile-id (:id profile2) + ::rpc/profile-id (:id profile2) :file-id (:id file2) :library-id (:id file1)} - out (th/mutation! data) + out (th/command! data) error (:error out)] ;; (th/print-result! out) @@ -350,11 +350,11 @@ :profile-id (:id profile2)}) data {::th/type :link-file-to-library - :profile-id (:id profile2) + ::rpc/profile-id (:id profile2) :file-id (:id file2) :library-id (:id file1)} - out (th/mutation! data) + out (th/command! data) error (:error out)] ;; (th/print-result! out) @@ -372,10 +372,10 @@ (t/is (= 0 (:processed result)))) ;; query the list of files - (let [data {::th/type :project-files - :project-id (:default-project-id profile1) - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-project-files + ::rpc/profile-id (:id profile1) + :project-id (:default-project-id profile1)} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] @@ -384,15 +384,15 @@ ;; Request file to be deleted (let [params {::th/type :delete-file :id (:id file) - :profile-id (:id profile1)} - out (th/mutation! params)] + ::rpc/profile-id (:id profile1)} + out (th/command! params)] (t/is (nil? (:error out)))) ;; query the list of files after soft deletion - (let [data {::th/type :project-files - :project-id (:default-project-id profile1) - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-project-files + ::rpc/profile-id (:id profile1) + :project-id (:default-project-id profile1)} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] @@ -403,10 +403,10 @@ (t/is (= 0 (:processed result)))) ;; query the list of file libraries of a after hard deletion - (let [data {::th/type :file-libraries - :file-id (:id file) - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-file-libraries + ::rpc/profile-id (:id profile1) + :file-id (:id file)} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] @@ -417,10 +417,10 @@ (t/is (= 1 (:processed result)))) ;; query the list of file libraries of a after hard deletion - (let [data {::th/type :file-libraries - :file-id (:id file) - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-file-libraries + ::rpc/profile-id (:id profile1) + :file-id (:id file)} + out (th/command! data)] ;; (th/print-result! out) (let [error (:error out) error-data (ex-data error)] @@ -483,11 +483,11 @@ (t/testing "RPC page query (rendering purposes)" ;; Query :page RPC method without passing page-id - (let [data {::th/type :page - :profile-id (:id prof) + (let [data {::th/type :get-page + ::rpc/profile-id (:id prof) :file-id (:id file) :components-v2 true} - {:keys [error result] :as out} (th/query! data)] + {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (map? result)) @@ -500,12 +500,12 @@ ) ;; Query :page RPC method with page-id - (let [data {::th/type :page - :profile-id (:id prof) + (let [data {::th/type :get-page + ::rpc/profile-id (:id prof) :file-id (:id file) :page-id page-id :components-v2 true} - {:keys [error result] :as out} (th/query! data)] + {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (map? result)) (t/is (contains? result :objects)) @@ -516,13 +516,13 @@ (t/is (contains? (:objects result) uuid/zero))) ;; Query :page RPC method with page-id and object-id - (let [data {::th/type :page - :profile-id (:id prof) + (let [data {::th/type :get-page + ::rpc/profile-id (:id prof) :file-id (:id file) :page-id page-id :object-id frame1-id :components-v2 true} - {:keys [error result] :as out} (th/query! data)] + {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (nil? error)) (t/is (map? result)) @@ -534,12 +534,12 @@ (t/is (not (contains? (:objects result) shape2-id)))) ;; Query :page RPC method with wrong params - (let [data {::th/type :page - :profile-id (:id prof) + (let [data {::th/type :get-page + ::rpc/profile-id (:id prof) :file-id (:id file) :object-id frame1-id :components-v2 true} - out (th/query! data)] + out (th/command! data)] (t/is (not (th/success? out))) (let [{:keys [type code]} (-> out :error ex-data)] @@ -551,21 +551,21 @@ (t/testing "RPC :file-data-for-thumbnail" ;; Insert a thumbnail data for the frame-id (let [data {::th/type :upsert-file-object-thumbnail - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :file-id (:id file) :object-id (str page-id frame1-id) :data "random-data-1"} - {:keys [error result] :as out} (th/mutation! data)] + {:keys [error result] :as out} (th/command! data)] (t/is (nil? error)) (t/is (nil? result))) ;; Check the result - (let [data {::th/type :file-data-for-thumbnail - :profile-id (:id prof) + (let [data {::th/type :get-file-data-for-thumbnail + ::rpc/profile-id (:id prof) :file-id (:id file) :components-v2 true} - {:keys [error result] :as out} (th/query! data)] + {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (map? result)) (t/is (contains? result :page)) @@ -578,21 +578,21 @@ ;; Delete thumbnail data (let [data {::th/type :upsert-file-object-thumbnail - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :file-id (:id file) :object-id (str page-id frame1-id) :data nil} - {:keys [error result] :as out} (th/mutation! data)] + {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (nil? error)) (t/is (nil? result))) ;; Check the result - (let [data {::th/type :file-data-for-thumbnail - :profile-id (:id prof) + (let [data {::th/type :get-file-data-for-thumbnail + ::rpc/profile-id (:id prof) :file-id (:id file) :components-v2 true} - {:keys [error result] :as out} (th/query! data)] + {:keys [error result] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (map? result)) (t/is (contains? result :page)) @@ -606,11 +606,11 @@ ;; insert object snapshot for known frame (let [data {::th/type :upsert-file-object-thumbnail - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :file-id (:id file) :object-id (str page-id frame1-id) :data "new-data"} - {:keys [error result] :as out} (th/mutation! data)] + {:keys [error result] :as out} (th/command! data)] (t/is (nil? error)) (t/is (nil? result))) @@ -629,11 +629,11 @@ ;; insert object snapshot for for unknown frame (let [data {::th/type :upsert-file-object-thumbnail - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :file-id (:id file) :object-id (str page-id (uuid/next)) :data "new-data-2"} - {:keys [error result] :as out} (th/mutation! data)] + {:keys [error result] :as out} (th/command! data)] (t/is (nil? error)) (t/is (nil? result))) @@ -661,8 +661,8 @@ :project-id (:default-project-id prof) :revn 2 :is-shared false}) - data {::th/type :file-thumbnail - :profile-id (:id prof) + data {::th/type :get-file-thumbnail + ::rpc/profile-id (:id prof) :file-id (:id file)}] (t/testing "query a thumbnail with single revn" @@ -673,7 +673,7 @@ :revn 1 :data "testvalue1"}) - (let [{:keys [result error] :as out} (th/query! data)] + (let [{:keys [result error] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (nil? error)) (t/is (= 4 (count result))) @@ -687,7 +687,7 @@ :revn 2 :data "testvalue2"}) - (let [{:keys [result error] :as out} (th/query! data)] + (let [{:keys [result error] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (nil? error)) (t/is (= 4 (count result))) @@ -695,7 +695,7 @@ (t/is (= 2 (:revn result)))) ;; Then query the specific revn - (let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))] + (let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))] ;; (th/print-result! out) (t/is (nil? error)) (t/is (= 4 (count result))) @@ -704,18 +704,18 @@ (t/testing "upsert file-thumbnail" (let [data {::th/type :upsert-file-thumbnail - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :file-id (:id file) :data "foobar" :props {:baz 1} :revn 2} - {:keys [result error] :as out} (th/mutation! data)] + {:keys [result error] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (nil? error)) (t/is (nil? result)))) (t/testing "query last result" - (let [{:keys [result error] :as out} (th/query! data)] + (let [{:keys [result error] :as out} (th/command! data)] ;; (th/print-result! out) (t/is (nil? error)) (t/is (= 4 (count result))) @@ -734,7 +734,7 @@ (t/is (= 1 (:processed res)))) ;; Then query the specific revn - (let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))] + (let [{:keys [result error] :as out} (th/command! (assoc data :revn 1))] (t/is (th/ex-of-type? error :not-found)) (t/is (th/ex-of-code? error :file-thumbnail-not-found)))) )) diff --git a/backend/test/backend_tests/rpc_media_test.clj b/backend/test/backend_tests/rpc_media_test.clj index 5fb6f99051..138ac4d6a7 100644 --- a/backend/test/backend_tests/rpc_media_test.clj +++ b/backend/test/backend_tests/rpc_media_test.clj @@ -6,10 +6,11 @@ (ns backend-tests.rpc-media-test (:require - [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.db :as db] + [app.rpc :as-alias rpc] [app.storage :as sto] + [backend-tests.helpers :as th] [clojure.test :as t] [datoteka.core :as fs])) @@ -134,3 +135,123 @@ (t/is (= "image/jpeg" (:mtype result))) (t/is (uuid? (:media-id result))) (t/is (uuid? (:thumbnail-id result)))))) + + +(t/deftest media-object-from-url-command + (let [prof (th/create-profile* 1) + proj (th/create-project* 1 {:profile-id (:id prof) + :team-id (:default-team-id prof)}) + file (th/create-file* 1 {:profile-id (:id prof) + :project-id (:default-project-id prof) + :is-shared false}) + url "https://raw.githubusercontent.com/uxbox/uxbox/develop/sample_media/images/unsplash/anna-pelzer.jpg" + params {::th/type :create-file-media-object-from-url + ::rpc/profile-id (:id prof) + :file-id (:id file) + :is-local true + :url url} + out (th/command! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [{:keys [media-id thumbnail-id] :as result} (:result out)] + (t/is (= (:id file) (:file-id result))) + (t/is (= 1024 (:width result))) + (t/is (= 683 (:height result))) + (t/is (= "image/jpeg" (:mtype result))) + (t/is (uuid? media-id)) + (t/is (uuid? thumbnail-id)) + (let [storage (:app.storage/storage th/*system*) + mobj1 @(sto/get-object storage media-id) + mobj2 @(sto/get-object storage thumbnail-id)] + (t/is (sto/storage-object? mobj1)) + (t/is (sto/storage-object? mobj2)) + (t/is (= 122785 (:size mobj1))) + ;; This is because in ubuntu 21.04 generates different + ;; thumbnail that in ubuntu 22.04. This hack should be removed + ;; when we all use the ubuntu 22.04 devenv image. + (t/is (or (= 3302 (:size mobj2)) + (= 3303 (:size mobj2)))))))) + +(t/deftest media-object-upload-command + (let [prof (th/create-profile* 1) + proj (th/create-project* 1 {:profile-id (:id prof) + :team-id (:default-team-id prof)}) + file (th/create-file* 1 {:profile-id (:id prof) + :project-id (:default-project-id prof) + :is-shared false}) + mfile {:filename "sample.jpg" + :path (th/tempfile "backend_tests/test_files/sample.jpg") + :mtype "image/jpeg" + :size 312043} + + params {::th/type :upload-file-media-object + ::rpc/profile-id (:id prof) + :file-id (:id file) + :is-local true + :name "testfile" + :content mfile} + out (th/command! params)] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (let [{:keys [media-id thumbnail-id] :as result} (:result out)] + (t/is (= (:id file) (:file-id result))) + (t/is (= 800 (:width result))) + (t/is (= 800 (:height result))) + (t/is (= "image/jpeg" (:mtype result))) + (t/is (uuid? media-id)) + (t/is (uuid? thumbnail-id)) + (let [storage (:app.storage/storage th/*system*) + mobj1 @(sto/get-object storage media-id) + mobj2 @(sto/get-object storage thumbnail-id)] + (t/is (sto/storage-object? mobj1)) + (t/is (sto/storage-object? mobj2)) + (t/is (= 312043 (:size mobj1))) + (t/is (= 3887 (:size mobj2))))) + )) + + +(t/deftest media-object-upload-idempotency-command + (let [prof (th/create-profile* 1) + proj (th/create-project* 1 {:profile-id (:id prof) + :team-id (:default-team-id prof)}) + file (th/create-file* 1 {:profile-id (:id prof) + :project-id (:default-project-id prof) + :is-shared false}) + mfile {:filename "sample.jpg" + :path (th/tempfile "backend_tests/test_files/sample.jpg") + :mtype "image/jpeg" + :size 312043} + + params {::th/type :upload-file-media-object + ::rpc/profile-id (:id prof) + :file-id (:id file) + :is-local true + :name "testfile" + :content mfile + :id (uuid/next)}] + + ;; First try + (let [{:keys [result error] :as out} (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? error)) + (t/is (= (:id params) (:id result))) + (t/is (= (:file-id params) (:file-id result))) + (t/is (= 800 (:width result))) + (t/is (= 800 (:height result))) + (t/is (= "image/jpeg" (:mtype result))) + (t/is (uuid? (:media-id result))) + (t/is (uuid? (:thumbnail-id result)))) + + ;; Second try + (let [{:keys [result error] :as out} (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? error)) + (t/is (= (:id params) (:id result))) + (t/is (= (:file-id params) (:file-id result))) + (t/is (= 800 (:width result))) + (t/is (= 800 (:height result))) + (t/is (= "image/jpeg" (:mtype result))) + (t/is (uuid? (:media-id result))) + (t/is (uuid? (:thumbnail-id result)))))) diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index 11ec08a889..d39d9fc492 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -21,7 +21,7 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) -(t/deftest invite-team-member +(t/deftest create-team-invitations (with-mocks [mock {:target 'app.emails/send! :return nil}] (let [profile1 (th/create-profile* 1 {:is-active true}) profile2 (th/create-profile* 2 {:is-active true}) @@ -30,14 +30,14 @@ team (th/create-team* 1 {:profile-id (:id profile1)}) pool (:app.db/pool th/*system*) - data {::th/type :invite-team-member + data {::th/type :create-team-invitations + ::rpc/profile-id (:id profile1) :team-id (:id team) - :role :editor - :profile-id (:id profile1)}] + :role :editor}] ;; invite external user without complaints (let [data (assoc data :email "foo@bar.com") - out (th/mutation! data) + out (th/command! data) ;; retrieve the value from the database and check its content invitation (db/exec-one! th/*pool* @@ -52,7 +52,7 @@ ;; invite internal user without complaints (th/reset-mock! mock) (let [data (assoc data :email (:email profile2)) - out (th/mutation! data)] + out (th/command! data)] (t/is (th/success? out)) (t/is (= 1 (:call-count (deref mock))))) @@ -60,7 +60,7 @@ (th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"}) (th/reset-mock! mock) (let [data (assoc data :email "foo@bar.com") - out (th/mutation! data)] + out (th/command! data)] (t/is (th/success? out)) (t/is (= 1 (:call-count (deref mock))))) @@ -79,7 +79,7 @@ (th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"}) (let [data (assoc data :email "foo@bar.com") - out (th/mutation! data)] + out (th/command! data)] (t/is (not (th/success? out))) (t/is (= 0 (:call-count @mock))) @@ -92,7 +92,7 @@ (th/reset-mock! mock) (let [data (assoc data :email (:email profile3)) - out (th/mutation! data)] + out (th/command! data)] (t/is (not (th/success? out))) (t/is (= 0 (:call-count @mock))) @@ -115,12 +115,12 @@ pool (:app.db/pool th/*system*)] ;; Try to invite a not existing user - (let [data {::th/type :invite-team-member + (let [data {::th/type :create-team-invitations + ::rpc/profile-id (:id profile1) :email "notexisting@example.com" :team-id (:id team) - :role :editor - :profile-id (:id profile1)} - out (th/mutation! data)] + :role :editor} + out (th/command! data)] ;; (th/print-result! out) (t/is (th/success? out)) @@ -139,12 +139,12 @@ (th/reset-mock! mock) ;; Try to invite existing user - (let [data {::th/type :invite-team-member + (let [data {::th/type :create-team-invitations + ::rpc/profile-id (:id profile1) :email (:email profile2) :team-id (:id team) - :role :editor - :profile-id (:id profile1)} - out (th/mutation! data)] + :role :editor} + out (th/command! data)] ;; (th/print-result! out) (t/is (th/success? out)) @@ -215,7 +215,9 @@ :role "editor" :valid-until (dt/in-future "48h")}) - (let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile2)} + (let [data {::th/type :verify-token + ::rpc/profile-id (:id profile2) + :token token} out (th/command! data)] ;; (th/print-result! out) (t/is (th/success? out)) @@ -236,7 +238,9 @@ :role "editor" :valid-until (dt/in-future "48h")}) - (let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile1)} + (let [data {::th/type :verify-token + ::rpc/profile-id (:id profile1) + :token token} out (th/command! data)] ;; (th/print-result! out) (t/is (not (th/success? out))) @@ -246,7 +250,7 @@ ))) -(t/deftest invite-team-member-with-email-verification-disabled +(t/deftest create-team-invitations-with-email-verification-disabled (with-mocks [mock {:target 'app.emails/send! :return nil}] (let [profile1 (th/create-profile* 1 {:is-active true}) profile2 (th/create-profile* 2 {:is-active true}) @@ -255,16 +259,16 @@ team (th/create-team* 1 {:profile-id (:id profile1)}) pool (:app.db/pool th/*system*) - data {::th/type :invite-team-member + data {::th/type :create-team-invitations + ::rpc/profile-id (:id profile1) :team-id (:id team) - :role :editor - :profile-id (:id profile1)}] + :role :editor}] ;; invite internal user without complaints (with-redefs [app.config/flags #{}] (th/reset-mock! mock) (let [data (assoc data :email (:email profile2)) - out (th/mutation! data)] + out (th/command! data)] (t/is (th/success? out)) (t/is (= 0 (:call-count (deref mock))))) @@ -279,8 +283,8 @@ team (th/create-team* 1 {:profile-id (:id profile1)}) pool (:app.db/pool th/*system*) data {::th/type :delete-team - :team-id (:id team) - :profile-id (:id profile1)}] + ::rpc/profile-id (:id profile1) + :team-id (:id team)}] ;; team is not deleted because it does not meet all ;; conditions to be deleted. @@ -288,9 +292,9 @@ (t/is (= 0 (:processed result)))) ;; query the list of teams - (let [data {::th/type :teams - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-teams + ::rpc/profile-id (:id profile1)} + out (th/command! data)] ;; (th/print-result! out) (t/is (th/success? out)) (let [result (:result out)] @@ -300,15 +304,15 @@ ;; Request team to be deleted (let [params {::th/type :delete-team - :id (:id team) - :profile-id (:id profile1)} - out (th/mutation! params)] + ::rpc/profile-id (:id profile1) + :id (:id team)} + out (th/command! params)] (t/is (th/success? out))) ;; query the list of teams after soft deletion - (let [data {::th/type :teams - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-teams + ::rpc/profile-id (:id profile1)} + out (th/command! data)] ;; (th/print-result! out) (t/is (th/success? out)) (let [result (:result out)] @@ -321,8 +325,8 @@ ;; query the list of projects after hard deletion (let [data {::th/type :projects - :team-id (:id team) - :profile-id (:id profile1)} + :profile-id (:id profile1) + :team-id (:id team)} out (th/query! data)] ;; (th/print-result! out) (t/is (not (th/success? out))) @@ -335,8 +339,8 @@ ;; query the list of projects of a after hard deletion (let [data {::th/type :projects - :team-id (:id team) - :profile-id (:id profile1)} + :profile-id (:id profile1) + :team-id (:id team)} out (th/query! data)] ;; (th/print-result! out) @@ -348,8 +352,8 @@ (t/deftest query-team-invitations (let [prof (th/create-profile* 1 {:is-active true}) team (th/create-team* 1 {:profile-id (:id prof)}) - data {::th/type :team-invitations - :profile-id (:id prof) + data {::th/type :get-team-invitations + ::rpc/profile-id (:id prof) :team-id (:id team)}] ;; insert an entry on the database with an enabled invitation @@ -366,7 +370,7 @@ :role "editor" :valid-until (dt/in-past "48h")}) - (let [out (th/query! data)] + (let [out (th/command! data)] (t/is (th/success? out)) (let [result (:result out) one (first result) @@ -381,7 +385,7 @@ (let [prof (th/create-profile* 1 {:is-active true}) team (th/create-team* 1 {:profile-id (:id prof)}) data {::th/type :update-team-invitation-role - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :team-id (:id team) :email "TEST1@mail.com" :role :admin}] @@ -393,7 +397,7 @@ :role "editor" :valid-until (dt/in-future "48h")}) - (let [out (th/mutation! data) + (let [out (th/command! data) ;; retrieve the value from the database and check its content res (db/get* th/*pool* :team-invitation {:team-id (:team-id data) :email-to "test1@mail.com"})] @@ -405,7 +409,7 @@ (let [prof (th/create-profile* 1 {:is-active true}) team (th/create-team* 1 {:profile-id (:id prof)}) data {::th/type :delete-team-invitation - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :team-id (:id team) :email "TEST1@mail.com"}] @@ -416,7 +420,7 @@ :role "editor" :valid-until (dt/in-future "48h")}) - (let [out (th/mutation! data) + (let [out (th/command! data) ;; retrieve the value from the database and check its content res (db/get* th/*pool* :team-invitation {:team-id (:team-id data) :email-to "test1@mail.com"})] diff --git a/common/deps.edn b/common/deps.edn index a8919e73db..2d94cc3227 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -23,7 +23,7 @@ com.cognitect/transit-cljs {:mvn/version "0.8.280"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} - funcool/promesa {:mvn/version "10.0.571"} + funcool/promesa {:mvn/version "10.0.594"} funcool/cuerdas {:mvn/version "2022.06.16-403"} lambdaisland/uri {:mvn/version "1.13.95" diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index 333af74723..0b36e532d6 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -89,9 +89,9 @@ (contains? data :explain)) (explain (:explain data) opts) - (and (::s/problems data) - (::s/value data) - (::s/spec data)) + (and (contains? data ::s/problems) + (contains? data ::s/value) + (contains? data ::s/spec)) (binding [s/*explain-out* expound/printer] (with-out-str (s/explain-out (update data ::s/problems #(take max-problems %)))))))) diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index 48d0e5baf9..6b9b469254 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -18,10 +18,10 @@ (def defaults {:public-uri "http://localhost:3449" - :tenant "dev" - :host "devenv" + :tenant "default" + :host "localhost" :http-server-port 6061 - :http-server-host "localhost" + :http-server-host "0.0.0.0" :redis-uri "redis://redis/0"}) (s/def ::http-server-port ::us/integer) diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 086a4bb951..8fd501f59e 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -93,7 +93,7 @@ (->> (rx/from uris) (rx/filter (comp not svg-url?)) (rx/map prepare) - (rx/mapcat #(rp/mutation! :create-file-media-object-from-url %)) + (rx/mapcat #(rp/command! :create-file-media-object-from-url %)) (rx/do on-image)) (->> (rx/from uris) diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 4f34e2d206..f299fa8d69 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -466,9 +466,10 @@ {:name (extract-name uri) :url uri})))) (rx/mapcat (fn [uri-data] - (->> (rp/mutation! (if (contains? uri-data :content) - :upload-file-media-object - :create-file-media-object-from-url) uri-data) + (->> (rp/command! (if (contains? uri-data :content) + :upload-file-media-object + :create-file-media-object-from-url) + uri-data) ;; When the image uploaded fail we skip the shape ;; returning `nil` will afterward not create the shape. (rx/catch #(rx/of nil))