From db689d151e98b9bb1ffc6703ba4e76e618708aed Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 2 Jan 2023 22:56:24 +0100 Subject: [PATCH 01/16] :recycle: Refactor profile and session handling - makes the profile access more efficient (replace in-app joins to a simple select query on profile table - add partial support for access-tokens (still missing some RPC methods) - move router definitions to specific modules and simplify the main http module definitions to simple includes - simplifiy authentication code related to access-tokens and sessions - normalize db parameters with proper namespaced props - more work on convert all modules initialization to use proper specs with fully-qualified keyword config props --- backend/resources/rlimit.edn | 6 +- backend/src/app/auth/oidc.clj | 34 +- backend/src/app/cli/manage.clj | 21 +- backend/src/app/config.clj | 2 + backend/src/app/db.clj | 116 +++---- backend/src/app/db/sql.clj | 7 +- backend/src/app/http.clj | 79 ++--- backend/src/app/http/access_token.clj | 96 ++++++ backend/src/app/http/assets.clj | 15 +- backend/src/app/http/awsns.clj | 17 +- backend/src/app/http/debug.clj | 47 +-- backend/src/app/http/errors.clj | 10 +- backend/src/app/http/feedback.clj | 3 +- backend/src/app/http/session.clj | 276 ++++++++------- backend/src/app/http/websocket.clj | 88 ++--- backend/src/app/loggers/audit.clj | 2 +- backend/src/app/loggers/webhooks.clj | 2 +- backend/src/app/main.clj | 144 ++++---- backend/src/app/metrics.clj | 13 + backend/src/app/migrations.clj | 6 + .../sql/0099-add-access-token-table.sql | 19 ++ .../sql/0100-mod-profile-indexes.sql | 34 ++ backend/src/app/redis.clj | 24 +- backend/src/app/rpc.clj | 322 ++++++++++-------- backend/src/app/rpc/climit.clj | 26 +- backend/src/app/rpc/commands/access_token.clj | 64 ++++ backend/src/app/rpc/commands/audit.clj | 2 +- backend/src/app/rpc/commands/auth.clj | 71 ++-- backend/src/app/rpc/commands/binfile.clj | 22 +- backend/src/app/rpc/commands/comments.clj | 22 +- backend/src/app/rpc/commands/demo.clj | 36 +- backend/src/app/rpc/commands/files.clj | 4 +- backend/src/app/rpc/commands/files/temp.clj | 9 +- backend/src/app/rpc/commands/files/update.clj | 4 +- backend/src/app/rpc/commands/ldap.clj | 17 +- backend/src/app/rpc/commands/media.clj | 11 +- backend/src/app/rpc/commands/teams.clj | 24 +- backend/src/app/rpc/commands/verify_token.clj | 19 +- backend/src/app/rpc/doc.clj | 2 + backend/src/app/rpc/mutations/fonts.clj | 5 +- backend/src/app/rpc/mutations/media.clj | 5 +- backend/src/app/rpc/mutations/profile.clj | 39 +-- backend/src/app/rpc/mutations/teams.clj | 3 +- backend/src/app/rpc/queries/profile.clj | 97 ++---- backend/src/app/rpc/queries/share_link.clj | 5 +- backend/src/app/rpc/quotes.clj | 23 +- backend/src/app/rpc/rlimit.clj | 205 ++++++----- backend/src/app/srepl/helpers.clj | 2 +- backend/src/app/srepl/main.clj | 16 +- backend/src/app/storage.clj | 4 +- backend/src/app/util/retry.clj | 2 +- backend/src/app/util/websocket.clj | 3 +- backend/src/app/worker.clj | 2 +- .../backend_tests/bounce_handling_test.clj | 2 +- backend/test/backend_tests/helpers.clj | 106 +++--- backend/test/backend_tests/rpc_file_test.clj | 4 +- .../test/backend_tests/rpc_profile_test.clj | 2 +- common/src/app/common/uuid.cljc | 7 + 58 files changed, 1285 insertions(+), 963 deletions(-) create mode 100644 backend/src/app/http/access_token.clj create mode 100644 backend/src/app/migrations/sql/0099-add-access-token-table.sql create mode 100644 backend/src/app/migrations/sql/0100-mod-profile-indexes.sql create mode 100644 backend/src/app/rpc/commands/access_token.clj diff --git a/backend/resources/rlimit.edn b/backend/resources/rlimit.edn index c7df92bdf..acb131bf1 100644 --- a/backend/resources/rlimit.edn +++ b/backend/resources/rlimit.edn @@ -3,8 +3,8 @@ {:default [[:default :window "200000/h"]] - #{:query/teams} + #{:command/get-teams} [[:burst :bucket "5/1/5s"]] - #{:query/profile} - [[:burst :bucket "100/60/1m"]]} + #{:command/get-profile} + [[:burst :bucket "60/60/1m"]]} diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index 8440b650e..98c9c0e8f 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -349,7 +349,7 @@ ::fullname ::props])) -(defn retrieve-info +(defn get-info [{:keys [provider] :as cfg} {:keys [params] :as request}] (letfn [(validate-oidc [info] ;; If the provider is OIDC, we can proceed to check @@ -396,14 +396,12 @@ (p/then' validate-oidc) (p/then' (partial post-process state)))))) -(defn- retrieve-profile +(defn- get-profile [{:keys [::db/pool ::wrk/executor] :as cfg} info] (px/with-dispatch executor (with-open [conn (db/open pool)] (some->> (:email info) - (profile/retrieve-profile-data-by-email conn) - (profile/populate-additional-data conn) - (profile/decode-profile-row))))) + (profile/get-profile-by-email conn))))) (defn- redirect-response [uri] @@ -417,9 +415,9 @@ (redirect-response uri))) (defn- generate-redirect - [{:keys [::session/session] :as cfg} request info profile] + [cfg request info profile] (if profile - (let [sxf (session/create-fn session (:id profile)) + (let [sxf (session/create-fn cfg (:id profile)) token (or (:invitation-token info) (tokens/generate (::main/props cfg) {:iss :auth @@ -436,7 +434,7 @@ (when-let [collector (::audit/collector cfg)] (audit/submit! collector {:type "command" - :name "login" + :name "login-with-password" :profile-id (:id profile) :ip-addr (audit/parse-client-ip request) :props (audit/profile->props profile)})) @@ -471,8 +469,8 @@ (defn- callback-handler [cfg request] (letfn [(process-request [] - (p/let [info (retrieve-info cfg request) - profile (retrieve-profile cfg info)] + (p/let [info (get-info cfg request) + profile (get-profile cfg info)] (generate-redirect cfg request info profile))) (handle-error [cause] @@ -524,23 +522,24 @@ (s/def ::providers (s/map-of ::us/keyword (s/nilable ::provider))) +(s/def ::routes vector?) + (defmethod ig/pre-init-spec ::routes [_] - (s/keys :req [::http/client + (s/keys :req [::session/manager + ::http/client ::wrk/executor ::main/props ::db/pool - ::providers - ::session/session])) + ::providers])) (defmethod ig/init-key ::routes - [_ {:keys [::wrk/executor ::session/session] :as cfg}] + [_ {:keys [::wrk/executor] :as cfg}] (let [cfg (update cfg :provider d/without-nils)] - ["" {:middleware [[(:middleware session)] + ["" {:middleware [[session/authz cfg] [hmw/with-dispatch executor] [hmw/with-config cfg] - [provider-lookup] - ]} + [provider-lookup]]} ["/auth/oauth" ["/:provider" {:handler auth-handler @@ -548,4 +547,3 @@ ["/:provider/callback" {:handler callback-handler :allowed-methods #{:get}}]]])) - diff --git a/backend/src/app/cli/manage.clj b/backend/src/app/cli/manage.clj index fedf040bc..8d992a210 100644 --- a/backend/src/app/cli/manage.clj +++ b/backend/src/app/cli/manage.clj @@ -10,9 +10,9 @@ [app.common.logging :as l] [app.db :as db] [app.main :as main] - [app.rpc.commands.auth :as cmd.auth] + [app.rpc.commands.auth :as auth] [app.rpc.mutations.profile :as profile] - [app.rpc.queries.profile :refer [retrieve-profile-data-by-email]] + [app.rpc.queries.profile :refer [get-profile-by-email]] [clojure.string :as str] [clojure.tools.cli :refer [parse-opts]] [integrant.core :as ig]) @@ -55,16 +55,17 @@ :type :password}))] (try (db/with-atomic [conn (:app.db/pool system)] - (->> (cmd.auth/create-profile conn - {:fullname fullname - :email email - :password password - :is-active true - :is-demo false}) - (cmd.auth/create-profile-relations conn))) + (->> (auth/create-profile! conn + {:fullname fullname + :email email + :password password + :is-active true + :is-demo false}) + (auth/create-profile-rels! conn))) (when (pos? (:verbosity options)) (println "User created successfully.")) + (System/exit 0) (catch Exception _e @@ -79,7 +80,7 @@ (db/with-atomic [conn (:app.db/pool system)] (let [email (or (:email options) (read-from-console {:label "Email:"})) - profile (retrieve-profile-data-by-email conn email)] + profile (get-profile-by-email conn email)] (when-not profile (when (pos? (:verbosity options)) (println "Profile does not exists.")) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 870ff58d5..3ce625f70 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -128,6 +128,7 @@ (s/def ::database-max-pool-size ::us/integer) (s/def ::quotes-teams-per-profile ::us/integer) +(s/def ::quotes-access-tokens-per-profile ::us/integer) (s/def ::quotes-projects-per-team ::us/integer) (s/def ::quotes-invitations-per-team ::us/integer) (s/def ::quotes-profiles-per-team ::us/integer) @@ -281,6 +282,7 @@ ::public-uri ::quotes-teams-per-profile + ::quotes-access-tokens-per-profile ::quotes-projects-per-team ::quotes-invitations-per-team ::quotes-profiles-per-team diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 045ee46a9..e5968b9b1 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -233,44 +233,46 @@ [pool] (jdbc/get-connection pool)) +(def ^:private default-opts + {:builder-fn sql/as-kebab-maps}) + (defn exec! ([ds sv] - (exec! ds sv {})) + (jdbc/execute! ds sv default-opts)) ([ds sv opts] - (jdbc/execute! ds sv (assoc opts :builder-fn sql/as-kebab-maps)))) + (jdbc/execute! ds sv (merge default-opts opts)))) (defn exec-one! - ([ds sv] (exec-one! ds sv {})) + ([ds sv] + (jdbc/execute-one! ds sv default-opts)) ([ds sv opts] - (jdbc/execute-one! ds sv (assoc opts :builder-fn sql/as-kebab-maps)))) + (jdbc/execute-one! ds sv + (-> (merge default-opts opts) + (assoc :return-keys (::return-keys? opts false)))))) (defn insert! - ([ds table params] (insert! ds table params nil)) - ([ds table params opts] - (exec-one! ds - (sql/insert table params opts) - (merge {:return-keys true} opts)))) + [ds table params & {:as opts}] + (exec-one! ds + (sql/insert table params opts) + (merge {::return-keys? true} opts))) (defn insert-multi! - ([ds table cols rows] (insert-multi! ds table cols rows nil)) - ([ds table cols rows opts] - (exec! ds - (sql/insert-multi table cols rows opts) - (merge {:return-keys true} opts)))) + [ds table cols rows & {:as opts}] + (exec! ds + (sql/insert-multi table cols rows opts) + (merge {::return-keys? true} opts))) (defn update! - ([ds table params where] (update! ds table params where nil)) - ([ds table params where opts] - (exec-one! ds - (sql/update table params where opts) - (merge {:return-keys true} opts)))) + [ds table params where & {:as opts}] + (exec-one! ds + (sql/update table params where opts) + (merge {::return-keys? true} opts))) (defn delete! - ([ds table params] (delete! ds table params nil)) - ([ds table params opts] - (exec-one! ds - (sql/delete table params opts) - (assoc opts :return-keys true)))) + [ds table params & {:as opts}] + (exec-one! ds + (sql/delete table params opts) + (merge {::return-keys? true} opts))) (defn is-row-deleted? [{:keys [deleted-at]}] @@ -279,56 +281,34 @@ (inst-ms (dt/now))))) (defn get* - "Internal function for retrieve a single row from database that - matches a simple filters." - ([ds table params] - (get* ds table params nil)) - ([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}] - (let [rows (exec! ds (sql/select table params opts)) - rows (cond->> rows - check-deleted? - (remove is-row-deleted?))] - (first rows)))) + "Retrieve a single row from database that matches a simple filters. Do + not raises exceptions." + [ds table params & {:as opts}] + (let [rows (exec! ds (sql/select table params opts)) + rows (cond->> rows + (::remove-deleted? opts true) + (remove is-row-deleted?))] + (first rows))) (defn get - ([ds table params] - (get ds table params nil)) - ([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}] - (let [row (get* ds table params opts)] - (when (and (not row) check-deleted?) - (ex/raise :type :not-found - :code :object-not-found - :table table - :hint "database object not found")) - row))) - -(defn get-by-params - "DEPRECATED" - ([ds table params] - (get-by-params ds table params nil)) - ([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}] - (let [row (get* ds table params (assoc opts :check-deleted? check-not-found))] - (when (and (not row) check-not-found) - (ex/raise :type :not-found - :code :object-not-found - :table table - :hint "database object not found")) - row))) + "Retrieve a single row from database that matches a simple + filters. Raises :not-found exception if no object is found." + [ds table params & {:as opts}] + (let [row (get* ds table params opts)] + (when (and (not row) (::check-deleted? opts true)) + (ex/raise :type :not-found + :code :object-not-found + :table table + :hint "database object not found")) + row)) (defn get-by-id - ([ds table id] - (get ds table {:id id} nil)) - ([ds table id opts] - (let [opts (cond-> opts - (contains? opts :check-not-found) - (assoc :check-deleted? (:check-not-found opts)))] - (get ds table {:id id} opts)))) + [ds table id & {:as opts}] + (get ds table {:id id} opts)) (defn query - ([ds table params] - (query ds table params nil)) - ([ds table params opts] - (exec! ds (sql/select table params opts)))) + [ds table params & {:as opts}] + (exec! ds (sql/select table params opts))) (defn pgobject? ([v] diff --git a/backend/src/app/db/sql.clj b/backend/src/app/db/sql.clj index 998d594c8..854b2211c 100644 --- a/backend/src/app/db/sql.clj +++ b/backend/src/app/db/sql.clj @@ -7,6 +7,7 @@ (ns app.db.sql (:refer-clojure :exclude [update]) (:require + [app.db :as-alias db] [clojure.string :as str] [next.jdbc.optional :as jdbc-opt] [next.jdbc.sql.builder :as sql])) @@ -43,8 +44,10 @@ ([table where-params opts] (let [opts (merge default-opts opts) opts (cond-> opts - (:for-update opts) (assoc :suffix "FOR UPDATE") - (:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))] + (::db/for-update? opts) (assoc :suffix "FOR UPDATE") + (::db/for-share? opts) (assoc :suffix "FOR KEY SHARE") + (:for-update opts) (assoc :suffix "FOR UPDATE") + (:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))] (sql/for-query table where-params opts)))) (defn update diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 6cb6b398c..f5091522e 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -6,13 +6,22 @@ (ns app.http (:require + [app.auth.oidc :as-alias oidc] [app.common.data :as d] [app.common.logging :as l] [app.common.transit :as t] + [app.db :as-alias db] + [app.http.access-token :as actoken] + [app.http.assets :as-alias assets] + [app.http.awsns :as-alias awsns] + [app.http.debug :as-alias debug] [app.http.errors :as errors] [app.http.middleware :as mw] [app.http.session :as session] + [app.http.websocket :as-alias ws] [app.metrics :as mtx] + [app.rpc :as-alias rpc] + [app.rpc.doc :as-alias rpc.doc] [app.worker :as wrk] [clojure.spec.alpha :as s] [integrant.core :as ig] @@ -64,7 +73,6 @@ :http/max-body-size (:max-body-size cfg) :http/max-multipart-body-size (:max-multipart-body-size cfg) :xnio/io-threads (:io-threads cfg) - :xnio/worker-threads (:worker-threads cfg) :xnio/dispatch (:executor cfg) :ring/async true} @@ -113,64 +121,41 @@ ;; HTTP ROUTER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::assets map?) -(s/def ::awsns-handler fn?) -(s/def ::debug-routes (s/nilable vector?)) -(s/def ::doc-routes (s/nilable vector?)) -(s/def ::feedback fn?) -(s/def ::oauth map?) -(s/def ::oidc-routes (s/nilable vector?)) -(s/def ::rpc-routes (s/nilable vector?)) -(s/def ::session ::session/session) -(s/def ::storage map?) -(s/def ::ws fn?) - (defmethod ig/pre-init-spec ::router [_] - (s/keys :req-un [::mtx/metrics - ::ws - ::storage - ::assets - ::session - ::feedback - ::awsns-handler - ::debug-routes - ::oidc-routes - ::rpc-routes - ::doc-routes])) + (s/keys :req [::session/manager + ::actoken/manager + ::ws/routes + ::rpc/routes + ::rpc.doc/routes + ::oidc/routes + ::assets/routes + ::debug/routes + ::db/pool + ::mtx/routes + ::awsns/routes])) (defmethod ig/init-key ::router - [_ {:keys [ws session metrics assets feedback] :as cfg}] + [_ cfg] (rr/router [["" {:middleware [[mw/server-timing] [mw/format-response] [mw/params] [mw/parse-request] - [session/middleware-1 session] + [session/soft-auth cfg] + [actoken/soft-auth cfg] [mw/errors errors/handle] [mw/restrict-methods]]} - ["/metrics" {:handler (::mtx/handler metrics) - :allowed-methods #{:get}}] - - ["/assets" {:middleware [[session/middleware-2 session]]} - ["/by-id/:id" {:handler (:objects-handler assets)}] - ["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}] - ["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]] - - (:debug-routes cfg) + (::mtx/routes cfg) + (::assets/routes cfg) + (::debug/routes cfg) ["/webhooks" - ["/sns" {:handler (:awsns-handler cfg) - :allowed-methods #{:post}}]] + (::awsns/routes cfg)] - ["/ws/notifications" {:middleware [[session/middleware-2 session]] - :handler ws - :allowed-methods #{:get}}] + (::ws/routes cfg) - ["/api" {:middleware [[mw/cors] - [session/middleware-2 session]]} - ["/feedback" {:handler feedback - :allowed-methods #{:post}}] - (:doc-routes cfg) - (:oidc-routes cfg) - (:rpc-routes cfg)]]])) + ["/api" {:middleware [[mw/cors]]} + (::oidc/routes cfg) + (::rpc.doc/routes cfg) + (::rpc/routes cfg)]]])) diff --git a/backend/src/app/http/access_token.clj b/backend/src/app/http/access_token.clj new file mode 100644 index 000000000..76cf07eef --- /dev/null +++ b/backend/src/app/http/access_token.clj @@ -0,0 +1,96 @@ +;; 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.http.access-token + (:require + [app.common.logging :as l] + [app.common.spec :as us] + [app.config :as cf] + [app.db :as db] + [app.main :as-alias main] + [app.tokens :as tokens] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [promesa.core :as p] + [promesa.exec :as px] + [yetti.request :as yrq])) + + +(s/def ::manager + (s/keys :req [::db/pool ::wrk/executor ::main/props])) + +(defmethod ig/pre-init-spec ::manager [_] ::manager) +(defmethod ig/init-key ::manager [_ cfg] cfg) +(defmethod ig/halt-key! ::manager [_ _]) + +(def header-re #"^Token\s+(.*)") + +(defn- get-token + [request] + (some->> (yrq/get-header request "authorization") + (re-matches header-re) + (second))) + +(defn- decode-token + [props token] + (when token + (tokens/verify props {:token token :iss "access-token"}))) + +(defn- get-token-perms + [pool token-id] + (when-not (db/read-only? pool) + (when-let [token (db/get* pool :access-token {:id token-id} {:columns [:perms]})] + (some-> (:perms token) + (db/decode-pgarray #{}))))) + +(defn- wrap-soft-auth + [handler {:keys [::manager]}] + (us/assert! ::manager manager) + + (let [{:keys [::wrk/executor ::main/props]} manager] + (fn [request respond raise] + (let [token (get-token request)] + (->> (px/submit! executor (partial decode-token props token)) + (p/fnly (fn [claims cause] + (when cause + (l/trace :hint "exception on decoding malformed token" :cause cause)) + (let [request (cond-> request + (map? claims) + (assoc ::id (:tid claims)))] + (handler request respond raise))))))))) + +(defn- wrap-authz + [handler {:keys [::manager]}] + (us/assert! ::manager manager) + (let [{:keys [::wrk/executor ::db/pool]} manager] + (fn [request respond raise] + (if-let [token-id (::id request)] + (->> (px/submit! executor (partial get-token-perms pool token-id)) + (p/fnly (fn [perms cause] + (cond + (some? cause) + (raise cause) + + (nil? perms) + (handler request respond raise) + + :else + (let [request (assoc request ::perms perms)] + (handler request respond raise)))))) + (handler request respond raise))))) + +(def soft-auth + {:name ::soft-auth + :compile (fn [& _] + (when (contains? cf/flags :access-tokens) + wrap-soft-auth))}) + +(def authz + {:name ::authz + :compile (fn [& _] + (when (contains? cf/flags :access-tokens) + wrap-authz))}) diff --git a/backend/src/app/http/assets.clj b/backend/src/app/http/assets.clj index 0fd6d1510..a5362bbf6 100644 --- a/backend/src/app/http/assets.clj +++ b/backend/src/app/http/assets.clj @@ -115,7 +115,10 @@ (s/def ::cache-max-age ::dt/duration) (s/def ::signature-max-age ::dt/duration) -(defmethod ig/pre-init-spec ::handlers [_] +(s/def ::routes vector?) + +;; FIXME: namespace qualified params +(defmethod ig/pre-init-spec ::routes [_] (s/keys :req-un [::storage ::wrk/executor ::mtx/metrics @@ -123,9 +126,9 @@ ::cache-max-age ::signature-max-age])) -(defmethod ig/init-key ::handlers +(defmethod ig/init-key ::routes [_ cfg] - {:objects-handler (partial objects-handler cfg) - :file-objects-handler (partial file-objects-handler cfg) - :file-thumbnails-handler (partial file-thumbnails-handler cfg)}) - + ["/assets" + ["/by-id/:id" {:handler (partial objects-handler cfg)}] + ["/by-file-media-id/:id" {:handler (partial file-objects-handler cfg)}] + ["/by-file-media-id/:id/thumbnail" {:handler (partial file-thumbnails-handler cfg)}]]) diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj index bf5f32aeb..7ae00779c 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -28,18 +28,20 @@ (declare parse-notification) (declare process-report) -(defmethod ig/pre-init-spec ::handler [_] +(defmethod ig/pre-init-spec ::routes [_] (s/keys :req [::http/client ::main/props ::db/pool ::wrk/executor])) -(defmethod ig/init-key ::handler +(defmethod ig/init-key ::routes [_ {:keys [::wrk/executor] :as cfg}] - (fn [request respond _] - (let [data (-> request yrq/body slurp)] - (px/run! executor #(handle-request cfg data))) - (respond (yrs/response 200)))) + (letfn [(handler [request respond _] + (let [data (-> request yrq/body slurp)] + (px/run! executor #(handle-request cfg data))) + (respond (yrs/response 200)))] + ["/sns" {:handler handler + :allowed-methods #{:post}}])) (defn handle-request [cfg data] @@ -105,8 +107,7 @@ [cfg headers] (let [tdata (get headers "x-penpot-data")] (when-not (str/empty? tdata) - (let [sprops (::main/props cfg) - result (tokens/verify sprops {:token tdata :iss :profile-identity})] + (let [result (tokens/verify (::main/props cfg) {:token tdata :iss :profile-identity})] (:profile-id result))))) (defn- parse-notification diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 4233909b4..083ebc9a5 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -39,9 +39,9 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn authorized? - [pool {:keys [profile-id]}] + [pool {:keys [::session/profile-id]}] (or (= "devenv" (cf/get :host)) - (let [profile (ex/ignoring (profile/retrieve-profile-data pool profile-id)) + (let [profile (ex/ignoring (profile/get-profile pool profile-id)) admins (or (cf/get :admins) #{})] (contains? admins (:email profile))))) @@ -61,7 +61,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn index-handler - [{:keys [pool]} request] + [{:keys [::db/pool]} request] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) @@ -81,7 +81,7 @@ "select revn, changes, data from file_change where file_id=? and revn = ?") (defn- retrieve-file-data - [{:keys [pool]} {:keys [params profile-id] :as request}] + [{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) @@ -107,8 +107,9 @@ (prepare-download-response data filename) (contains? params :clone) - (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id) - data (some-> data blob/decode)] + (let [profile (profile/get-profile pool profile-id) + project-id (:default-project-id profile) + data (blob/decode data)] (create-file pool {:id (uuid/next) :name (str "Cloned file: " filename) :project-id project-id @@ -117,7 +118,7 @@ (yrs/response 201 "OK CREATED")) :else - (prepare-response (some-> data blob/decode)))))) + (prepare-response (blob/decode data)))))) (defn- is-file-exists? [pool id] @@ -125,8 +126,9 @@ (-> (db/exec-one! pool [sql id]) :exists))) (defn- upload-file-data - [{:keys [pool]} {:keys [profile-id params] :as request}] - (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id) + [{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}] + (let [profile (profile/get-profile pool profile-id) + project-id (:default-project-id profile) data (some-> params :file :path io/read-as-bytes blob/decode)] (if (and data project-id) @@ -162,7 +164,7 @@ :code :method-not-found))) (defn file-changes-handler - [{:keys [pool]} {:keys [params] :as request}] + [{:keys [::db/pool]} {:keys [params] :as request}] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) @@ -202,7 +204,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn error-handler - [{:keys [pool]} request] + [{:keys [::db/pool]} request] (letfn [(parse-id [request] (let [id (get-in request [:path-params :id]) id (parse-uuid id)] @@ -251,7 +253,7 @@ LIMIT 100") (defn error-list-handler - [{:keys [pool]} request] + [{:keys [::db/pool]} request] (when-not (authorized? pool request) (ex/raise :type :authentication :code :only-admins-allowed)) @@ -268,7 +270,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn export-handler - [{:keys [pool] :as cfg} {:keys [params profile-id] :as request}] + [{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}] (let [file-ids (->> (:file-ids params) (remove empty?) @@ -287,7 +289,8 @@ (assoc ::binf/include-libraries? libs?) (binf/export-to-tmpfile!))] (if clone? - (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)] + (let [profile (profile/get-profile pool profile-id) + project-id (:default-project-id profile)] (binf/import! (assoc cfg ::binf/input path @@ -309,15 +312,16 @@ (defn import-handler - [{:keys [pool] :as cfg} {:keys [params profile-id] :as request}] + [{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}] (when-not (contains? params :file) (ex/raise :type :validation :code :missing-upload-file :hint "missing upload file")) - (let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id) + (let [profile (profile/get-profile pool profile-id) + project-id (:default-project-id profile) overwrite? (contains? params :overwrite) - migrate? (contains? params :migrate) + migrate? (contains? params :migrate) ignore-index-errors? (contains? params :ignore-index-errors)] (when-not project-id @@ -381,16 +385,17 @@ (raise (ex/error :type :authentication :code :only-admins-allowed))))))}) - (defmethod ig/pre-init-spec ::routes [_] - (s/keys :req-un [::db/pool ::wrk/executor ::session/session])) + (s/keys :req [::db/pool + ::wrk/executor + ::session/manager])) (defmethod ig/init-key ::routes - [_ {:keys [session pool executor] :as cfg}] + [_ {:keys [::db/pool ::wrk/executor] :as cfg}] [["/readyz" {:middleware [[mw/with-dispatch executor] [mw/with-config cfg]] :handler health-handler}] - ["/dbg" {:middleware [[session/middleware-2 session] + ["/dbg" {:middleware [[session/authz cfg] [with-authorization pool] [mw/with-dispatch executor] [mw/with-config cfg]]} diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 6e7a2b748..eafb55d14 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -11,6 +11,8 @@ [app.common.exceptions :as ex] [app.common.logging :as l] [app.http :as-alias http] + [app.http.access-token :as-alias actoken] + [app.http.session :as-alias session] [clojure.spec.alpha :as s] [cuerdas.core :as str] [yetti.request :as yrq] @@ -26,7 +28,9 @@ (defn get-context [request] - (let [claims (:session-token-claims request)] + (let [claims (-> {} + (into (::session/token-claims request)) + (into (::actoken/token-claims request)))] (merge *context* {:path (:path request) @@ -49,6 +53,10 @@ [err _] (yrs/response 401 (ex-data err))) +(defmethod handle-exception :authorization + [err _] + (yrs/response 403 (ex-data err))) + (defmethod handle-exception :restriction [err _] (yrs/response 400 (ex-data err))) diff --git a/backend/src/app/http/feedback.clj b/backend/src/app/http/feedback.clj index beaffc753..e602b533b 100644 --- a/backend/src/app/http/feedback.clj +++ b/backend/src/app/http/feedback.clj @@ -13,6 +13,7 @@ [app.config :as cf] [app.db :as db] [app.emails :as eml] + [app.http.session :as-alias session] [app.rpc.queries.profile :as profile] [app.worker :as wrk] [clojure.spec.alpha :as s] @@ -42,7 +43,7 @@ :hint "feedback module is disabled")))))) (defn- handler - [{:keys [pool] :as cfg} {:keys [profile-id] :as request}] + [{:keys [pool] :as cfg} {:keys [::session/profile-id] :as request}] (let [ftoken (cf/get :feedback-token ::no-token) token (yrq/get-header request "x-feedback-token") params (d/merge (:params request) diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index 5d136ac44..f3860b7ee 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -9,14 +9,17 @@ (:require [app.common.data :as d] [app.common.logging :as l] + [app.common.spec :as us] [app.config :as cf] [app.db :as db] [app.db.sql :as sql] + [app.http.session.tasks :as-alias tasks] [app.main :as-alias main] [app.tokens :as tokens] [app.util.time :as dt] [app.worker :as wrk] [clojure.spec.alpha :as s] + [cuerdas.core :as str] [integrant.core :as ig] [promesa.core :as p] [promesa.exec :as px] @@ -45,55 +48,55 @@ (defprotocol ISessionManager (read [_ key]) - (decode [_ key]) (write! [_ key data]) (update! [_ data]) (delete! [_ key])) -(s/def ::session #(satisfies? ISessionManager %)) +(s/def ::manager #(satisfies? ISessionManager %)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; STORAGE IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(s/def ::session-params + (s/keys :req-un [::user-agent + ::profile-id + ::created-at])) + (defn- prepare-session-params - [props data] - (let [profile-id (:profile-id data) - user-agent (:user-agent data) - created-at (or (:created-at data) (dt/now)) - token (tokens/generate props {:iss "authentication" - :iat created-at - :uid profile-id})] - {:user-agent user-agent - :profile-id profile-id - :created-at created-at - :updated-at created-at - :id token})) + [key params] + (us/assert! ::us/not-empty-string key) + (us/assert! ::session-params params) + + {:user-agent (:user-agent params) + :profile-id (:profile-id params) + :created-at (:created-at params) + :updated-at (:created-at params) + :id key}) (defn- database-manager [{:keys [::db/pool ::wrk/executor ::main/props]}] + ^{::wrk/executor executor + ::db/pool pool + ::main/props props} (reify ISessionManager (read [_ token] (px/with-dispatch executor (db/exec-one! pool (sql/select :http-session {:id token})))) - (decode [_ token] + (write! [_ key params] (px/with-dispatch executor - (tokens/verify props {:token token :iss "authentication"}))) - - (write! [_ _ data] - (px/with-dispatch executor - (let [params (prepare-session-params props data)] + (let [params (prepare-session-params key params)] (db/insert! pool :http-session params) params))) - (update! [_ data] + (update! [_ params] (let [updated-at (dt/now)] (px/with-dispatch executor (db/update! pool :http-session {:updated-at updated-at} - {:id (:id data)}) - (assoc data :updated-at updated-at)))) + {:id (:id params)}) + (assoc params :updated-at updated-at)))) (delete! [_ token] (px/with-dispatch executor @@ -101,27 +104,26 @@ nil)))) (defn inmemory-manager - [{:keys [::wrk/executor ::main/props]}] + [{:keys [::db/pool ::wrk/executor ::main/props]}] (let [cache (atom {})] + ^{::main/props props + ::wrk/executor executor + ::db/pool pool} (reify ISessionManager (read [_ token] (p/do (get @cache token))) - (decode [_ token] - (px/with-dispatch executor - (tokens/verify props {:token token :iss "authentication"}))) - - (write! [_ _ data] + (write! [_ key params] (p/do - (let [{:keys [token] :as params} (prepare-session-params props data)] - (swap! cache assoc token params) + (let [params (prepare-session-params key params)] + (swap! cache assoc key params) params))) - (update! [_ data] + (update! [_ params] (p/do (let [updated-at (dt/now)] - (swap! cache update (:id data) assoc :updated-at updated-at) - (assoc data :updated-at updated-at)))) + (swap! cache update (:id params) assoc :updated-at updated-at) + (assoc params :updated-at updated-at)))) (delete! [_ token] (p/do @@ -144,25 +146,34 @@ ;; MANAGER IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare assign-auth-token-cookie) -(declare assign-authenticated-cookie) -(declare clear-auth-token-cookie) -(declare clear-authenticated-cookie) +(declare ^:private assign-auth-token-cookie) +(declare ^:private assign-authenticated-cookie) +(declare ^:private clear-auth-token-cookie) +(declare ^:private clear-authenticated-cookie) +(declare ^:private gen-token) (defn create-fn - [manager profile-id] - (fn [request response] - (let [uagent (yrq/get-header request "user-agent") - params {:profile-id profile-id - :user-agent uagent}] - (-> (write! manager nil params) - (p/then (fn [session] - (l/trace :hint "create" :profile-id profile-id) - (-> response - (assign-auth-token-cookie session) - (assign-authenticated-cookie session)))))))) + [{:keys [::manager]} profile-id] + (us/assert! ::manager manager) + (us/assert! ::us/uuid profile-id) + + (let [props (-> manager meta ::main/props)] + (fn [request response] + (let [uagent (yrq/get-header request "user-agent") + params {:profile-id profile-id + :user-agent uagent + :created-at (dt/now)} + token (gen-token props params)] + + (->> (write! manager token params) + (p/fmap (fn [session] + (l/trace :hint "create" :profile-id profile-id) + (-> response + (assign-auth-token-cookie session) + (assign-authenticated-cookie session))))))))) (defn delete-fn - [manager] + [{:keys [::manager]}] + (us/assert! ::manager manager) (letfn [(delete [{:keys [profile-id] :as request}] (let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name) cookie (yrq/get-cookie request cname)] @@ -177,68 +188,92 @@ (clear-auth-token-cookie) (clear-authenticated-cookie)))))) -(def middleware-1 - (letfn [(decode-cookie [manager cookie] - (if-let [value (:value cookie)] - (decode manager value) - (p/resolved nil))) +(defn- gen-token + [props {:keys [profile-id created-at]}] + (tokens/generate props {:iss "authentication" + :iat created-at + :uid profile-id})) +(defn- decode-token + [props token] + (when token + (tokens/verify props {:token token :iss "authentication"}))) - (wrap-handler [manager handler request respond raise] - (let [cookie (some->> (cf/get :auth-token-cookie-name default-auth-token-cookie-name) - (yrq/get-cookie request))] - (->> (decode-cookie manager cookie) - (p/fnly (fn [claims _] - (cond-> request - (some? claims) (assoc :session-token-claims claims) - :always (handler respond raise)))))))] - {:name :session-1 - :compile (fn [& _] - (fn [handler manager] - (partial wrap-handler manager handler)))})) +(defn- get-token + [request] + (let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name) + cookie (some-> (yrq/get-cookie request cname) :value)] + (when-not (str/empty? cookie) + cookie))) -(def middleware-2 - (letfn [(wrap-handler [manager handler request respond raise] - (-> (retrieve-session manager request) - (p/finally (fn [session cause] - (cond - (some? cause) - (raise cause) +(defn- get-session + [manager token] + (some->> token (read manager))) - (nil? session) - (handler request respond raise) +(defn- renew-session? + [{:keys [updated-at] :as session}] + (and (dt/instant? updated-at) + (let [elapsed (dt/diff updated-at (dt/now))] + (neg? (compare default-renewal-max-age elapsed))))) - :else - (let [request (-> request - (assoc :profile-id (:profile-id session)) - (assoc :session-id (:id session))) - respond (cond-> respond - (renew-session? session) - (wrap-respond manager session))] - (handler request respond raise))))))) +(defn- wrap-reneval + [respond manager session] + (fn [response] + (p/let [session (update! manager session)] + (-> response + (assign-auth-token-cookie session) + (assign-authenticated-cookie session) + (respond))))) - (retrieve-session [manager request] - (let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name) - cookie (yrq/get-cookie request cname)] - (some->> (:value cookie) (read manager)))) +(defn- wrap-soft-auth + [handler {:keys [::manager]}] + (us/assert! ::manager manager) - (renew-session? [{:keys [updated-at] :as session}] - (and (dt/instant? updated-at) - (let [elapsed (dt/diff updated-at (dt/now))] - (neg? (compare default-renewal-max-age elapsed))))) + (let [{:keys [::wrk/executor ::main/props]} (meta manager)] + (fn [request respond raise] + (let [token (get-token request)] + (->> (px/submit! executor (partial decode-token props token)) + (p/fnly (fn [claims cause] + (when cause + (l/trace :hint "exception on decoding malformed token" :cause cause)) - ;; Wrap respond with session renewal code - (wrap-respond [respond manager session] - (fn [response] - (p/let [session (update! manager session)] - (-> response - (assign-auth-token-cookie session) - (assign-authenticated-cookie session) - (respond)))))] + (let [request (cond-> request + (map? claims) + (-> (assoc ::token-claims claims) + (assoc ::token token)))] + (handler request respond raise))))))))) - {:name :session-2 - :compile (fn [& _] - (fn [handler manager] - (partial wrap-handler manager handler)))})) +(defn- wrap-authz + [handler {:keys [::manager]}] + (us/assert! ::manager manager) + (fn [request respond raise] + (if-let [token (::token request)] + (->> (get-session manager token) + (p/fnly (fn [session cause] + (cond + (some? cause) + (raise cause) + + (nil? session) + (handler request respond raise) + + :else + (let [request (-> request + (assoc ::profile-id (:profile-id session)) + (assoc ::id (:id session))) + respond (cond-> respond + (renew-session? session) + (wrap-reneval manager session))] + (handler request respond raise)))))) + + (handler request respond raise)))) + +(def soft-auth + {:name ::soft-auth + :compile (constantly wrap-soft-auth)}) + +(def authz + {:name ::authz + :compile (constantly wrap-authz)}) ;; --- IMPL @@ -300,21 +335,26 @@ ;; TASK: SESSION GC ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare sql:delete-expired) +(s/def ::tasks/max-age ::dt/duration) -(s/def ::max-age ::dt/duration) +(defmethod ig/pre-init-spec ::tasks/gc [_] + (s/keys :req [::db/pool] + :opt [::tasks/max-age])) -(defmethod ig/pre-init-spec ::gc-task [_] - (s/keys :req-un [::db/pool] - :opt-un [::max-age])) - -(defmethod ig/prep-key ::gc-task +(defmethod ig/prep-key ::tasks/gc [_ cfg] - (merge {:max-age default-cookie-max-age} - (d/without-nils cfg))) + (let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)] + (merge {::tasks/max-age max-age} (d/without-nils cfg)))) -(defmethod ig/init-key ::gc-task - [_ {:keys [pool max-age] :as cfg}] +(def ^:private + sql:delete-expired + "delete from http_session + where updated_at < now() - ?::interval + or (updated_at is null and + created_at < now() - ?::interval)") + +(defmethod ig/init-key ::tasks/gc + [_ {:keys [::db/pool ::tasks/max-age] :as cfg}] (l/debug :hint "initializing session gc task" :max-age max-age) (fn [_] (db/with-atomic [conn pool] @@ -326,9 +366,3 @@ :deleted result) result)))) -(def ^:private - sql:delete-expired - "delete from http_session - where updated_at < now() - ?::interval - or (updated_at is null and - created_at < now() - ?::interval)") diff --git a/backend/src/app/http/websocket.clj b/backend/src/app/http/websocket.clj index 7e16d74f6..f06fd1d7c 100644 --- a/backend/src/app/http/websocket.clj +++ b/backend/src/app/http/websocket.clj @@ -12,6 +12,7 @@ [app.common.pprint :as pp] [app.common.spec :as us] [app.db :as db] + [app.http.session :as session] [app.metrics :as mtx] [app.msgbus :as mbus] [app.util.time :as dt] @@ -34,7 +35,7 @@ (def state (atom {})) (defn- on-connect - [{:keys [metrics]} wsp] + [{:keys [::mtx/metrics]} wsp] (let [created-at (dt/now)] (swap! state assoc (::ws/id @wsp) wsp) (mtx/run! metrics @@ -48,7 +49,7 @@ :val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0))))) (defn- on-rcv-message - [{:keys [metrics]} _ message] + [{:keys [::mtx/metrics]} _ message] (mtx/run! metrics :id :websocket-messages-total :labels recv-labels @@ -56,7 +57,7 @@ message) (defn- on-snd-message - [{:keys [metrics]} _ message] + [{:keys [::mtx/metrics]} _ message] (mtx/run! metrics :id :websocket-messages-total :labels send-labels @@ -95,7 +96,6 @@ :user-agent (::ws/user-agent @wsp) :ip-addr (::ws/remote-addr @wsp) :last-activity-at (::ws/last-activity-at @wsp) - :http-session-id (::ws/http-session-id @wsp) :subscribed-file (-> wsp deref ::file-subscription :file-id) :subscribed-team (-> wsp deref ::team-subscription :team-id)})) @@ -120,7 +120,7 @@ (defmethod handle-message :connect [cfg wsp _] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) conn-id (::ws/id @wsp) profile-id (::profile-id @wsp) session-id (::session-id @wsp) @@ -139,7 +139,7 @@ (defmethod handle-message :disconnect [cfg wsp _] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) conn-id (::ws/id @wsp) profile-id (::profile-id @wsp) session-id (::session-id @wsp) @@ -173,7 +173,7 @@ (defmethod handle-message :subscribe-team [cfg wsp {:keys [team-id] :as params}] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) conn-id (::ws/id @wsp) session-id (::session-id @wsp) output-ch (::ws/output-ch @wsp) @@ -205,7 +205,7 @@ (defmethod handle-message :subscribe-file [cfg wsp {:keys [file-id] :as params}] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) conn-id (::ws/id @wsp) profile-id (::profile-id @wsp) session-id (::session-id @wsp) @@ -258,7 +258,7 @@ (defmethod handle-message :unsubscribe-file [cfg wsp {:keys [file-id] :as params}] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) conn-id (::ws/id @wsp) session-id (::session-id @wsp) profile-id (::profile-id @wsp) @@ -288,7 +288,7 @@ (defmethod handle-message :pointer-update [cfg wsp {:keys [file-id] :as message}] - (let [msgbus (:msgbus cfg) + (let [msgbus (::mbus/msgbus cfg) profile-id (::profile-id @wsp) session-id (::session-id @wsp) subs (::file-subscription @wsp) @@ -313,39 +313,47 @@ ;; HTTP HANDLER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::msgbus ::mbus/msgbus) (s/def ::session-id ::us/uuid) - (s/def ::handler-params (s/keys :req-un [::session-id])) -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::msgbus ::db/pool ::mtx/metrics])) +(defn- http-handler + [cfg {:keys [params ::session/profile-id] :as request} respond raise] + (let [{:keys [session-id]} (us/conform ::handler-params params)] + (cond + (not profile-id) + (raise (ex/error :type :authentication + :hint "Authentication required.")) -(defmethod ig/init-key ::handler + (not (yws/upgrade-request? request)) + (raise (ex/error :type :validation + :code :websocket-request-expected + :hint "this endpoint only accepts websocket connections")) + + :else + (do + (l/trace :hint "websocket request" :profile-id profile-id :session-id session-id) + + (->> (ws/handler + ::ws/on-rcv-message (partial on-rcv-message cfg) + ::ws/on-snd-message (partial on-snd-message cfg) + ::ws/on-connect (partial on-connect cfg) + ::ws/handler (partial handle-message cfg) + ::profile-id profile-id + ::session-id session-id) + (yws/upgrade request) + (respond)))))) + +(defmethod ig/pre-init-spec ::routes [_] + (s/keys :req [::mbus/msgbus + ::mtx/metrics + ::db/pool + ::session/manager])) + +(s/def ::routes vector?) + +(defmethod ig/init-key ::routes [_ cfg] - (fn [{:keys [profile-id params] :as req} respond raise] - (let [{:keys [session-id]} (us/conform ::handler-params params)] - (cond - (not profile-id) - (raise (ex/error :type :authentication - :hint "Authentication required.")) - - (not (yws/upgrade-request? req)) - (raise (ex/error :type :validation - :code :websocket-request-expected - :hint "this endpoint only accepts websocket connections")) - - :else - (do - (l/trace :hint "websocket request" :profile-id profile-id :session-id session-id) - - (->> (ws/handler - ::ws/on-rcv-message (partial on-rcv-message cfg) - ::ws/on-snd-message (partial on-snd-message cfg) - ::ws/on-connect (partial on-connect cfg) - ::ws/handler (partial handle-message cfg) - ::profile-id profile-id - ::session-id session-id) - (yws/upgrade req) - (respond))))))) + ["/ws/notifications" {:middleware [[session/authz cfg]] + :handler (partial http-handler cfg) + :allowed-methods #{:get}}]) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 97664c280..a795d29e7 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -159,7 +159,7 @@ ;; 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"} + ::rtry/label "persist-audit-log"} (let [now (dt/now)] (db/insert! pool :audit-log (-> params diff --git a/backend/src/app/loggers/webhooks.clj b/backend/src/app/loggers/webhooks.clj index dc19fc3ae..a3015a4fb 100644 --- a/backend/src/app/loggers/webhooks.clj +++ b/backend/src/app/loggers/webhooks.clj @@ -111,7 +111,7 @@ " where id=?") err (:id whook)] - res (db/exec-one! pool sql {:return-keys true})] + res (db/exec-one! pool sql {::db/return-keys? true})] (when (>= (:error-count res) max-errors) (db/update! pool :webhook {:is-active false} {:id (:id whook)}))) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 6b5141a84..f6fdaebbf 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -12,15 +12,24 @@ [app.common.logging :as l] [app.config :as cf] [app.db :as-alias db] + [app.http.access-token :as-alias actoken] + [app.http.assets :as-alias http.assets] + [app.http.awsns :as http.awsns] [app.http.client :as-alias http.client] - [app.http.session :as-alias http.session] + [app.http.debug :as-alias http.debug] + [app.http.session :as-alias session] + [app.http.session.tasks :as-alias session.tasks] + [app.http.websocket :as http.ws] [app.loggers.audit :as-alias audit] [app.loggers.audit.tasks :as-alias audit.tasks] [app.loggers.webhooks :as-alias webhooks] [app.loggers.zmq :as-alias lzmq] [app.metrics :as-alias mtx] [app.metrics.definition :as-alias mdef] + [app.msgbus :as-alias mbus] [app.redis :as-alias rds] + [app.rpc :as-alias rpc] + [app.rpc.doc :as-alias rpc.doc] [app.storage :as-alias sto] [app.util.time :as dt] [app.worker :as-alias wrk] @@ -180,6 +189,9 @@ ::mtx/metrics {:default default-metrics} + ::mtx/routes + {::mtx/metrics (ig/ref ::mtx/metrics)} + :app.migrations/all {:main (ig/ref :app.migrations/migrations)} @@ -187,7 +199,7 @@ {::rds/uri (cf/get :redis-uri) ::mtx/metrics (ig/ref ::mtx/metrics)} - :app.msgbus/msgbus + ::mbus/msgbus {:backend (cf/get :msgbus-backend :redis) :executor (ig/ref ::wrk/executor) :redis (ig/ref ::rds/redis)} @@ -207,16 +219,20 @@ ::http.client/client {::wrk/executor (ig/ref ::wrk/executor)} - :app.http.session/manager + ::session/manager {::db/pool (ig/ref ::db/pool) ::wrk/executor (ig/ref ::wrk/executor) ::props (ig/ref :app.setup/props)} - :app.http.session/gc-task - {:pool (ig/ref ::db/pool) - :max-age (cf/get :auth-token-cookie-max-age)} + ::actoken/manager + {::db/pool (ig/ref ::db/pool) + ::wrk/executor (ig/ref ::wrk/executor) + ::props (ig/ref :app.setup/props)} - :app.http.awsns/handler + ::session.tasks/gc + {::db/pool (ig/ref ::db/pool)} + + ::http.awsns/routes {::props (ig/ref :app.setup/props) ::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client) @@ -259,50 +275,44 @@ {::http.client/client (ig/ref ::http.client/client)} ::oidc/routes - {::http.client/client (ig/ref ::http.client/client) - ::db/pool (ig/ref ::db/pool) - ::props (ig/ref :app.setup/props) - ::wrk/executor (ig/ref ::wrk/executor) - ::oidc/providers {:google (ig/ref ::oidc.providers/google) - :github (ig/ref ::oidc.providers/github) - :gitlab (ig/ref ::oidc.providers/gitlab) - :oidc (ig/ref ::oidc.providers/generic)} - ::audit/collector (ig/ref ::audit/collector) - ::http.session/session (ig/ref :app.http.session/manager)} + {::http.client/client (ig/ref ::http.client/client) + ::db/pool (ig/ref ::db/pool) + ::props (ig/ref :app.setup/props) + ::wrk/executor (ig/ref ::wrk/executor) + ::oidc/providers {:google (ig/ref ::oidc.providers/google) + :github (ig/ref ::oidc.providers/github) + :gitlab (ig/ref ::oidc.providers/gitlab) + :oidc (ig/ref ::oidc.providers/generic)} + ::audit/collector (ig/ref ::audit/collector) + ::session/manager (ig/ref ::session/manager)} - - ;; TODO: revisit the dependencies of this service, looks they are too much unused of them :app.http/router - {:assets (ig/ref :app.http.assets/handlers) - :feedback (ig/ref :app.http.feedback/handler) - :session (ig/ref :app.http.session/manager) - :awsns-handler (ig/ref :app.http.awsns/handler) - :debug-routes (ig/ref :app.http.debug/routes) - :oidc-routes (ig/ref ::oidc/routes) - :ws (ig/ref :app.http.websocket/handler) - :metrics (ig/ref ::mtx/metrics) - :public-uri (cf/get :public-uri) - :storage (ig/ref ::sto/storage) - :rpc-routes (ig/ref :app.rpc/routes) - :doc-routes (ig/ref :app.rpc.doc/routes) - :executor (ig/ref ::wrk/executor)} + {::session/manager (ig/ref ::session/manager) + ::actoken/manager (ig/ref ::actoken/manager) + ::wrk/executor (ig/ref ::wrk/executor) + ::db/pool (ig/ref ::db/pool) + ::rpc/routes (ig/ref ::rpc/routes) + ::rpc.doc/routes (ig/ref ::rpc.doc/routes) + ::props (ig/ref :app.setup/props) + ::mtx/routes (ig/ref ::mtx/routes) + ::oidc/routes (ig/ref ::oidc/routes) + ::http.debug/routes (ig/ref ::http.debug/routes) + ::http.assets/routes (ig/ref ::http.assets/routes) + ::http.ws/routes (ig/ref ::http.ws/routes) + ::http.awsns/routes (ig/ref ::http.awsns/routes)} :app.http.debug/routes - {:pool (ig/ref ::db/pool) - :executor (ig/ref ::wrk/executor) - :storage (ig/ref ::sto/storage) - :session (ig/ref :app.http.session/manager) + {::db/pool (ig/ref ::db/pool) + ::wrk/executor (ig/ref ::wrk/executor) + ::session/manager (ig/ref ::session/manager)} - ::db/pool (ig/ref ::db/pool) - ::wrk/executor (ig/ref ::wrk/executor) - ::sto/storage (ig/ref ::sto/storage)} + :app.http.websocket/routes + {::db/pool (ig/ref ::db/pool) + ::mtx/metrics (ig/ref ::mtx/metrics) + ::mbus/msgbus (ig/ref :app.msgbus/msgbus) + ::session/manager (ig/ref ::session/manager)} - :app.http.websocket/handler - {:pool (ig/ref ::db/pool) - :metrics (ig/ref ::mtx/metrics) - :msgbus (ig/ref :app.msgbus/msgbus)} - - :app.http.assets/handlers + :app.http.assets/routes {:metrics (ig/ref ::mtx/metrics) :assets-path (cf/get :assets-path) :storage (ig/ref ::sto/storage) @@ -310,37 +320,32 @@ :cache-max-age (dt/duration {:hours 24}) :signature-max-age (dt/duration {:hours 24 :minutes 5})} - :app.http.feedback/handler - {:pool (ig/ref ::db/pool) - :executor (ig/ref ::wrk/executor)} - :app.rpc/climit - {:metrics (ig/ref ::mtx/metrics) - :executor (ig/ref ::wrk/executor)} + {::mtx/metrics (ig/ref ::mtx/metrics) + ::wrk/executor (ig/ref ::wrk/executor)} :app.rpc/rlimit - {:executor (ig/ref ::wrk/executor) - :scheduled-executor (ig/ref ::wrk/scheduled-executor)} + {::wrk/executor (ig/ref ::wrk/executor) + ::wrk/scheduled-executor (ig/ref ::wrk/scheduled-executor)} :app.rpc/methods {::audit/collector (ig/ref ::audit/collector) ::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) + ::session/manager (ig/ref ::session/manager) ::ldap/provider (ig/ref ::ldap/provider) + ::sto/storage (ig/ref ::sto/storage) + ::mtx/metrics (ig/ref ::mtx/metrics) + ::mbus/msgbus (ig/ref ::mbus/msgbus) + ::rds/redis (ig/ref ::rds/redis) + + ::rpc/climit (ig/ref ::rpc/climit) + ::rpc/rlimit (ig/ref ::rpc/rlimit) + + ::props (ig/ref :app.setup/props) + :pool (ig/ref ::db/pool) - :session (ig/ref :app.http.session/manager) - :sprops (ig/ref :app.setup/props) - :metrics (ig/ref ::mtx/metrics) - :storage (ig/ref ::sto/storage) - :msgbus (ig/ref :app.msgbus/msgbus) - :public-uri (cf/get :public-uri) - :redis (ig/ref ::rds/redis) - :http-client (ig/ref ::http.client/client) - :climit (ig/ref :app.rpc/climit) - :rlimit (ig/ref :app.rpc/rlimit) - :executor (ig/ref ::wrk/executor) :templates (ig/ref :app.setup/builtin-templates) } @@ -348,7 +353,12 @@ {:methods (ig/ref :app.rpc/methods)} :app.rpc/routes - {:methods (ig/ref :app.rpc/methods)} + {::rpc/methods (ig/ref :app.rpc/methods) + ::db/pool (ig/ref ::db/pool) + ::wrk/executor (ig/ref ::wrk/executor) + ::session/manager (ig/ref ::session/manager) + ::actoken/manager (ig/ref ::actoken/manager) + ::props (ig/ref :app.setup/props)} ::wrk/registry {:metrics (ig/ref ::mtx/metrics) @@ -361,7 +371,7 @@ :storage-gc-touched (ig/ref ::sto/gc-touched-task) :tasks-gc (ig/ref :app.tasks.tasks-gc/handler) :telemetry (ig/ref :app.tasks.telemetry/handler) - :session-gc (ig/ref :app.http.session/gc-task) + :session-gc (ig/ref ::session.tasks/gc) :audit-log-archive (ig/ref ::audit.tasks/archive) :audit-log-gc (ig/ref ::audit.tasks/gc) diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj index c11d3d112..e23e47133 100644 --- a/backend/src/app/metrics.clj +++ b/backend/src/app/metrics.clj @@ -87,6 +87,7 @@ ::definitions definitions ::registry registry})) + (defn- handler [registry _ respond _] (let [samples (.metricFamilySamples ^CollectorRegistry registry) @@ -95,6 +96,18 @@ (respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004} :body (.toString writer)}))) + + +(s/def ::routes vector?) +(defmethod ig/pre-init-spec ::routes [_] + (s/keys :req [::metrics])) + +(defmethod ig/init-key ::routes + [_ {:keys [::metrics]}] + (let [registry (::registry metrics)] + ["/metrics" {:handler (partial handler registry) + :allowed-methods #{:get}}])) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 14c83afeb..f1f36e0dc 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -302,6 +302,12 @@ {:name "0098-add-quotes-table" :fn (mg/resource "app/migrations/sql/0098-add-quotes-table.sql")} + {:name "0099-add-access-token-table" + :fn (mg/resource "app/migrations/sql/0099-add-access-token-table.sql")} + + {:name "0100-mod-profile-indexes" + :fn (mg/resource "app/migrations/sql/0100-mod-profile-indexes.sql")} + ]) diff --git a/backend/src/app/migrations/sql/0099-add-access-token-table.sql b/backend/src/app/migrations/sql/0099-add-access-token-table.sql new file mode 100644 index 000000000..b46951cdc --- /dev/null +++ b/backend/src/app/migrations/sql/0099-add-access-token-table.sql @@ -0,0 +1,19 @@ +DROP TABLE IF EXISTS access_token; +CREATE TABLE access_token ( + id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY, + profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE, + + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + + name text NOT NULL, + token text NOT NULL, + perms text[] NULL +); + +ALTER TABLE access_token + ALTER COLUMN name SET STORAGE external, + ALTER COLUMN token SET STORAGE external, + ALTER COLUMN perms SET STORAGE external; + +CREATE INDEX access_token__profile_id__idx ON access_token(profile_id); diff --git a/backend/src/app/migrations/sql/0100-mod-profile-indexes.sql b/backend/src/app/migrations/sql/0100-mod-profile-indexes.sql new file mode 100644 index 000000000..a27313a0a --- /dev/null +++ b/backend/src/app/migrations/sql/0100-mod-profile-indexes.sql @@ -0,0 +1,34 @@ +DROP INDEX profile__email__idx; +CREATE INDEX profile__email__idx ON profile(email); + +ALTER TABLE profile + ADD COLUMN default_project_id uuid NULL REFERENCES project(id) ON DELETE SET NULL DEFERRABLE, + ADD COLUMN default_team_id uuid NULL REFERENCES team(id) ON DELETE SET NULL DEFERRABLE; + +CREATE INDEX profile__default_project__idx ON profile(default_project_id); +CREATE INDEX profile__default_team__idx ON profile(default_team_id); + +with profiles as ( + select p.id, + tpr.team_id as default_team_id, + ppr.project_id as default_project_id + from profile as p + join team_profile_rel as tpr + on (tpr.profile_id = p.id and + tpr.is_owner is true) + join project_profile_rel as ppr + on (ppr.profile_id = p.id and + ppr.is_owner is true) + join project as pj + on (pj.id = ppr.project_id) + join team as tm + on (tm.id = tpr.team_id) + where pj.is_default is true + and tm.is_default is true + and pj.team_id = tm.id +) +update profile + set default_team_id = p.default_team_id, + default_project_id = p.default_project_id + from profiles as p + where profile.id = p.id; diff --git a/backend/src/app/redis.clj b/backend/src/app/redis.clj index 0ada4b0b9..b00d51c7c 100644 --- a/backend/src/app/redis.clj +++ b/backend/src/app/redis.clj @@ -193,6 +193,7 @@ (defn get-or-connect [{:keys [::cache] :as state} key options] + (us/assert! ::redis state) (-> state (assoc ::connection (or (get @cache key) @@ -205,7 +206,6 @@ (defn add-listener! [{:keys [::connection] :as conn} listener] - (us/assert! ::connection-holder conn) (us/assert! ::pubsub-connection connection) (us/assert! ::pubsub-listener listener) (.addListener ^StatefulRedisPubSubConnection @connection @@ -213,10 +213,9 @@ conn) (defn publish! - [{:keys [::connection] :as conn} topic message] + [{:keys [::connection]} topic message] (us/assert! ::us/string topic) (us/assert! ::us/bytes message) - (us/assert! ::connection-holder conn) (us/assert! ::default-connection connection) (let [pcomm (.async ^StatefulRedisConnection @connection)] @@ -224,8 +223,7 @@ (defn subscribe! "Blocking operation, intended to be used on a thread/agent thread." - [{:keys [::connection] :as conn} & topics] - (us/assert! ::connection-holder conn) + [{:keys [::connection]} & topics] (us/assert! ::pubsub-connection connection) (try (let [topics (into-array String (map str topics)) @@ -236,8 +234,7 @@ (defn unsubscribe! "Blocking operation, intended to be used on a thread/agent thread." - [{:keys [::connection] :as conn} & topics] - (us/assert! ::connection-holder conn) + [{:keys [::connection]} & topics] (us/assert! ::pubsub-connection connection) (try (let [topics (into-array String (map str topics)) @@ -247,8 +244,8 @@ (throw (InterruptedException. (ex-message cause)))))) (defn rpush! - [{:keys [::connection] :as conn} key payload] - (us/assert! ::connection-holder conn) + [{:keys [::connection]} key payload] + (us/assert! ::default-connection connection) (us/assert! (or (and (vector? payload) (every? bytes? payload)) (bytes? payload))) @@ -270,8 +267,8 @@ (throw (InterruptedException. (ex-message cause)))))) (defn blpop! - [{:keys [::connection] :as conn} timeout & keys] - (us/assert! ::connection-holder conn) + [{:keys [::connection]} timeout & keys] + (us/assert! ::default-connection connection) (try (let [keys (into-array Object (map str keys)) cmd (.sync ^StatefulRedisConnection @connection) @@ -286,8 +283,7 @@ (throw (InterruptedException. (ex-message cause)))))) (defn open? - [{:keys [::connection] :as conn}] - (us/assert! ::connection-holder conn) + [{:keys [::connection]}] (us/assert! ::pubsub-connection connection) (.isOpen ^StatefulConnection @connection)) @@ -335,7 +331,7 @@ (defn eval! [{:keys [::mtx/metrics ::connection] :as state} script] (us/assert! ::redis state) - (us/assert! ::connection-holder state) + (us/assert! ::default-connection connection) (us/assert! ::rscript/script script) (let [cmd (.async ^StatefulRedisConnection @connection) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index cdda84a0b..a78601865 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -12,12 +12,15 @@ [app.common.logging :as l] [app.common.spec :as us] [app.common.uuid :as uuid] + [app.config :as cf] [app.db :as db] [app.http :as-alias http] + [app.http.access-token :as-alias actoken] [app.http.client :as-alias http.client] - [app.http.session :as-alias http.session] + [app.http.session :as-alias session] [app.loggers.audit :as audit] [app.loggers.webhooks :as-alias webhooks] + [app.main :as-alias main] [app.metrics :as mtx] [app.msgbus :as-alias mbus] [app.rpc.climit :as climit] @@ -71,71 +74,77 @@ (defn- rpc-query-handler "Ring handler that dispatches query requests and convert between internal async flow into ring async flow." - [methods {:keys [profile-id session-id path-params params] :as request} respond raise] - (let [type (keyword (:type path-params)) - data (-> params - (assoc ::request-at (dt/now)) - (assoc ::http/request request)) - data (if profile-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)] + [methods {:keys [params path-params] :as request} respond raise] + (let [type (keyword (:type path-params)) + profile-id (or (::session/profile-id request) + (::actoken/profile-id request)) - (-> (method data) - (p/then (partial handle-response request)) - (p/then respond) - (p/catch (fn [cause] - (let [context {:profile-id profile-id}] - (raise (ex/wrap-with-context cause context)))))))) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::http/request request)) + data (if profile-id + (-> data + (assoc :profile-id profile-id) + (assoc ::profile-id profile-id)) + (dissoc data :profile-id ::profile-id)) + method (get methods type default-handler)] + + (->> (method data) + (p/mcat (partial handle-response request)) + (p/fnly (fn [response cause] + (if cause + (raise (ex/wrap-with-context cause {:profile-id profile-id})) + (respond response))))))) (defn- rpc-mutation-handler "Ring handler that dispatches mutation requests and convert between internal async flow into ring async flow." - [methods {:keys [profile-id session-id path-params params] :as request} respond raise] - (let [type (keyword (:type path-params)) - data (-> params - (assoc ::request-at (dt/now)) - (assoc ::http/request request)) - data (if profile-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)) - (p/then respond) - (p/catch (fn [cause] - (let [context {:profile-id profile-id}] - (raise (ex/wrap-with-context cause context)))))))) + [methods {:keys [params path-params] :as request} respond raise] + (let [type (keyword (:type path-params)) + profile-id (or (::session/profile-id request) + (::actoken/profile-id request)) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::http/request request)) + data (if profile-id + (-> data + (assoc :profile-id profile-id) + (assoc ::profile-id profile-id)) + (dissoc data :profile-id)) + method (get methods type default-handler)] + + (->> (method data) + (p/mcat (partial handle-response request)) + (p/fnly (fn [response cause] + (if cause + (raise (ex/wrap-with-context cause {:profile-id profile-id})) + (respond response))))))) (defn- rpc-command-handler "Ring handler that dispatches cmd requests and convert between internal async flow into ring async flow." - [methods {:keys [profile-id session-id path-params params] :as request} respond raise] - (let [cmd (keyword (:type path-params)) - etag (yrq/get-header request "if-none-match") + [methods {:keys [params path-params] :as request} respond raise] + (let [type (keyword (:type path-params)) + etag (yrq/get-header request "if-none-match") + profile-id (or (::session/profile-id request) + (::actoken/profile-id request)) - 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)))) + data (-> params + (assoc ::request-at (dt/now)) + (assoc ::http/request request) + (assoc ::cond/key etag) + (cond-> (uuid? profile-id) + (assoc ::profile-id profile-id))) + + method (get methods type default-handler)] - method (get methods cmd default-handler)] (binding [cond/*enabled* true] - (-> (method data) - (p/then (partial handle-response request)) - (p/then respond) - (p/catch (fn [cause] - (let [context {:profile-id profile-id}] - (raise (ex/wrap-with-context cause context))))))))) + (->> (method data) + (p/mcat (partial handle-response request)) + (p/fnly (fn [response cause] + (if cause + (raise (ex/wrap-with-context cause {:profile-id profile-id})) + (respond response)))))))) (defn- wrap-metrics "Wrap service method with metrics measurement." @@ -143,18 +152,46 @@ (let [labels (into-array String [(::sv/name mdata)])] (fn [cfg params] (let [tp (dt/tpoint)] - (p/finally - (f cfg params) - (fn [_ _] - (mtx/run! metrics - :id metrics-id - :val (inst-ms (tp)) - :labels labels))))))) + (->> (f cfg params) + (p/fnly (fn [_ _] + (mtx/run! metrics + :id metrics-id + :val (inst-ms (tp)) + :labels labels)))))))) + + +(defn- wrap-authentication + [_ f {:keys [::auth] :as mdata}] + (fn [cfg params] + (let [profile-id (::profile-id params)] + (if (and auth (not (uuid? profile-id))) + (p/rejected + (ex/error :type :authentication + :code :authentication-required + :hint "authentication required for this endpoint")) + (f cfg params))))) + +(defn- wrap-access-token + "Wraps service method with access token validation." + [_ f {:keys [::sv/name] :as mdata}] + (if (contains? cf/flags :access-tokens) + (fn [cfg params] + (let [request (::http/request params)] + (if (contains? request ::actoken/id) + (let [perms (::actoken/perms request #{})] + (if (contains? perms name) + (f cfg params) + (p/rejected + (ex/error :type :authorization + :code :operation-not-allowed + :allowed perms)))) + (f cfg params)))) + f)) (defn- wrap-dispatch "Wraps service method into async flow, with the ability to dispatching it to a preconfigured executor service." - [{:keys [executor] :as cfg} f mdata] + [{:keys [::wrk/executor] :as cfg} f mdata] (with-meta (fn [cfg params] (->> (px/submit! executor (px/wrap-bindings #(f cfg params))) @@ -222,37 +259,34 @@ f)) f)) +(defn- wrap-spec-conform + [_ f mdata] + (let [spec (or (::sv/spec mdata) (s/spec any?))] + (fn [cfg params] + (let [params (ex/try! (us/conform spec params))] + (if (ex/exception? params) + (p/rejected params) + (f cfg params)))))) + +(defn- wrap-all + [cfg f mdata] + (as-> f $ + (wrap-dispatch cfg $ mdata) + (wrap-metrics cfg $ mdata) + (cond/wrap cfg $ mdata) + (retry/wrap-retry cfg $ mdata) + (climit/wrap cfg $ mdata) + (rlimit/wrap cfg $ mdata) + (wrap-audit cfg $ mdata) + (wrap-spec-conform cfg $ mdata) + (wrap-authentication cfg $ mdata) + (wrap-access-token cfg $ mdata))) + (defn- wrap [cfg f mdata] - (let [f (as-> f $ - (wrap-dispatch cfg $ mdata) - (cond/wrap cfg $ mdata) - (retry/wrap-retry cfg $ mdata) - (wrap-metrics cfg $ mdata) - (climit/wrap cfg $ mdata) - (rlimit/wrap cfg $ mdata) - (wrap-audit cfg $ mdata)) - - spec (or (::sv/spec mdata) (s/spec any?)) - auth? (::auth mdata true)] - - - (l/debug :hint "register method" :name (::sv/name mdata)) - (with-meta - (fn [params] - ;; Raise authentication error when rpc method requires auth but - ;; no profile-id is found in the request. - (let [profile-id (if (= "command" (::type cfg)) - (::profile-id params) - (:profile-id params))] - (p/do! - (if (and auth? (not (uuid? profile-id))) - (ex/raise :type :authentication - :code :authentication-required - :hint "authentication required for this endpoint") - (let [params (us/conform spec params)] - (f cfg params)))))) - mdata))) + (l/debug :hint "register method" :name (::sv/name mdata)) + (let [f (wrap-all cfg f mdata)] + (with-meta #(f cfg %) mdata))) (defn- process-method [cfg vfn] @@ -263,74 +297,70 @@ (defn- resolve-query-methods [cfg] (let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)] - (->> (sv/scan-ns 'app.rpc.queries.projects - 'app.rpc.queries.files - 'app.rpc.queries.teams - 'app.rpc.queries.profile - 'app.rpc.queries.viewer - 'app.rpc.queries.fonts) + (->> (sv/scan-ns + 'app.rpc.queries.projects + 'app.rpc.queries.files + 'app.rpc.queries.teams + 'app.rpc.queries.profile + 'app.rpc.queries.viewer + 'app.rpc.queries.fonts) (map (partial process-method cfg)) (into {})))) (defn- resolve-mutation-methods [cfg] (let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)] - (->> (sv/scan-ns 'app.rpc.mutations.media - 'app.rpc.mutations.profile - 'app.rpc.mutations.files - 'app.rpc.mutations.projects - 'app.rpc.mutations.teams - 'app.rpc.mutations.fonts - 'app.rpc.mutations.share-link) + (->> (sv/scan-ns + 'app.rpc.mutations.media + 'app.rpc.mutations.profile + 'app.rpc.mutations.files + 'app.rpc.mutations.projects + 'app.rpc.mutations.teams + 'app.rpc.mutations.fonts + 'app.rpc.mutations.share-link) (map (partial process-method cfg)) (into {})))) (defn- resolve-command-methods [cfg] (let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)] - (->> (sv/scan-ns 'app.rpc.commands.binfile - 'app.rpc.commands.comments - '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 - 'app.rpc.commands.demo - 'app.rpc.commands.webhooks - 'app.rpc.commands.audit - 'app.rpc.commands.files - 'app.rpc.commands.files.update - 'app.rpc.commands.files.create - 'app.rpc.commands.files.temp) + (->> (sv/scan-ns + 'app.rpc.commands.access-token + 'app.rpc.commands.audit + 'app.rpc.commands.auth + 'app.rpc.commands.binfile + 'app.rpc.commands.comments + 'app.rpc.commands.demo + 'app.rpc.commands.files + 'app.rpc.commands.files.create + 'app.rpc.commands.files.temp + 'app.rpc.commands.files.update + 'app.rpc.commands.ldap + 'app.rpc.commands.management + 'app.rpc.commands.media + 'app.rpc.commands.profile + 'app.rpc.commands.search + 'app.rpc.commands.teams + 'app.rpc.commands.verify-token + 'app.rpc.commands.webhooks) (map (partial process-method cfg)) (into {})))) -(s/def ::ldap (s/nilable map?)) -(s/def ::msgbus ::mbus/msgbus) -(s/def ::climit (s/nilable ::climit/climit)) -(s/def ::rlimit (s/nilable ::rlimit/rlimit)) - -(s/def ::public-uri ::us/not-empty-string) -(s/def ::sprops map?) - (defmethod ig/pre-init-spec ::methods [_] (s/keys :req [::audit/collector + ::session/manager ::http.client/client ::db/pool + ::mbus/msgbus ::ldap/provider - ::wrk/executor] - :req-un [::sto/storage - ::http.session/session - ::sprops - ::public-uri - ::msgbus - ::rlimit - ::climit - ::wrk/executor - ::mtx/metrics - ::db/pool])) + ::sto/storage + ::mtx/metrics + ::main/props + ::wrk/executor + ] + :opt [::climit + ::rlimit] + :req-un [::db/pool])) (defmethod ig/init-key ::methods [_ cfg] @@ -352,12 +382,20 @@ ::queries ::commands])) +(s/def ::routes vector?) + (defmethod ig/pre-init-spec ::routes [_] - (s/keys :req-un [::methods])) + (s/keys :req [::methods + ::db/pool + ::main/props + ::wrk/executor + ::session/manager + ::actoken/manager])) (defmethod ig/init-key ::routes - [_ {:keys [methods] :as cfg}] - [["/rpc" + [_ {:keys [::methods] :as cfg}] + [["/rpc" {:middleware [[session/authz cfg] + [actoken/authz cfg]]} ["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}] ["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}] ["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods)) diff --git a/backend/src/app/rpc/climit.clj b/backend/src/app/rpc/climit.clj index 8ccf07300..ade2eb6ae 100644 --- a/backend/src/app/rpc/climit.clj +++ b/backend/src/app/rpc/climit.clj @@ -46,7 +46,7 @@ (p/rejected (ex/error :type :internal :code :concurrency-limit-reached - :queue (-> limiter meta :bkey name) + :queue (-> limiter meta ::bkey name) :cause cause)) (some? cause) @@ -56,7 +56,7 @@ (p/resolved result)))))) (defn- create-limiter - [{:keys [executor metrics concurrency queue-size bkey skey]}] + [{:keys [::wrk/executor ::mtx/metrics ::bkey ::skey concurrency queue-size]}] (let [labels (into-array String [(name bkey)]) on-queue (fn [instance] (l/trace :hint "enqueued" @@ -100,10 +100,10 @@ :on-run on-run}] (-> (pxb/create options) - (vary-meta assoc :bkey bkey :skey skey)))) + (vary-meta assoc ::bkey bkey ::skey skey)))) (defn- create-cache - [{:keys [executor] :as params} config] + [{:keys [::wrk/executor] :as params} config] (let [listener (reify RemovalListener (onRemoval [_ key _val cause] (l/trace :hint "cache: remove" :key key :reason (str cause)))) @@ -113,8 +113,8 @@ (let [[bkey skey] key] (when-let [config (get config bkey)] (-> (merge params config) - (assoc :bkey bkey) - (assoc :skey skey) + (assoc ::bkey bkey) + (assoc ::skey skey) (create-limiter))))))] (.. (Caffeine/newBuilder) @@ -134,14 +134,16 @@ (defmethod ig/prep-key ::rpc/climit [_ cfg] - (merge {:path (cf/get :rpc-climit-config)} + (merge {::path (cf/get :rpc-climit-config)} (d/without-nils cfg))) +(s/def ::path ::fs/path) + (defmethod ig/pre-init-spec ::rpc/climit [_] - (s/keys :req-un [::wrk/executor ::mtx/metrics ::fs/path])) + (s/keys :req [::wrk/executor ::mtx/metrics ::path])) (defmethod ig/init-key ::rpc/climit - [_ {:keys [path] :as params}] + [_ {:keys [::path] :as params}] (when (contains? cf/flags :rpc-climit) (if-let [config (some->> path slurp edn/read-string)] (do @@ -163,7 +165,8 @@ (l/warn :hint "unable to load configuration" :config (str path))))) -(s/def ::climit #(satisfies? IConcurrencyManager %)) +(s/def ::rpc/climit + (s/nilable #(satisfies? IConcurrencyManager %))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PUBLIC API @@ -176,7 +179,7 @@ (p/wrap (do ~@body)))) (defn wrap - [{:keys [climit]} f {:keys [::queue ::key-fn] :as mdata}] + [{:keys [::rpc/climit]} f {:keys [::queue ::key-fn] :as mdata}] (if (and (some? climit) (some? queue)) (if-let [config (get @climit queue)] @@ -192,7 +195,6 @@ (let [key [queue (key-fn params)] lim (get climit key)] (invoke! lim (partial f cfg params)))) - (let [lim (get climit queue)] (fn [cfg params] (invoke! lim (partial f cfg params)))))) diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj new file mode 100644 index 000000000..48a10d128 --- /dev/null +++ b/backend/src/app/rpc/commands/access_token.clj @@ -0,0 +1,64 @@ +;; 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.access-token + (:require + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.db :as db] + [app.main :as-alias main] + [app.rpc :as-alias rpc] + [app.rpc.doc :as-alias doc] + [app.rpc.quotes :as quotes] + [app.tokens :as tokens] + [app.util.services :as sv] + [app.util.time :as dt] + [clojure.spec.alpha :as s])) + +(defn- create-access-token + [{:keys [::conn ::main/props]} profile-id name perms] + (let [created-at (dt/now) + token-id (uuid/next) + token (tokens/generate props {:iss "access-token" + :tid token-id + :iat created-at})] + (db/insert! conn :access-token + {:id token-id + :name name + :token token + :profile-id profile-id + :created-at created-at + :updated-at created-at + :perms (db/create-array conn "text" perms)}))) + +(defn repl-create-access-token + [{:keys [::db/pool] :as system} profile-id name perms] + (db/with-atomic [conn pool] + (let [props (:app.setup/props system)] + (create-access-token {::conn conn ::main/props props} + profile-id + name + perms)))) + +(s/def ::name ::us/not-empty-string) +(s/def ::perms ::us/set-of-strings) + +(s/def ::create-access-token + (s/keys :req [::rpc/profile-id] + :req-un [::name ::perms])) + +(sv/defmethod ::create-access-token + {::doc/added "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name perms]}] + (db/with-atomic [conn pool] + (let [cfg (assoc cfg ::conn conn)] + (quotes/check-quote! conn + {::quotes/id ::quotes/access-tokens-per-profile + ::quotes/profile-id profile-id}) + (create-access-token cfg profile-id name perms)))) + + + diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index b12e12b22..9e5e4c76a 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -42,7 +42,7 @@ :profile-id :ip-addr :props :context]) (defn- handle-events - [{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request] :as params}] + [{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request]}] (let [ip-addr (audit/parse-client-ip request) xform (comp (map #(assoc % :profile-id profile-id)) diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index dec65f571..4a037b18b 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -69,7 +69,7 @@ ;; ---- COMMAND: login with password (defn login-with-password - [{:keys [::db/pool session] :as cfg} {:keys [email password] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [email password] :as params}] (when-not (or (contains? cf/flags :login) (contains? cf/flags :login-with-password)) @@ -105,11 +105,10 @@ profile)] (db/with-atomic [conn pool] - (let [profile (->> (profile/retrieve-profile-data-by-email conn email) + (let [profile (->> (profile/get-profile-by-email conn email) (validate-profile) - (profile/strip-private-attrs) - (profile/populate-additional-data conn) - (profile/decode-profile-row)) + (profile/decode-row) + (profile/strip-private-attrs)) invitation (when-let [token (:invitation-token params)] (tokens/verify (::main/props cfg) {:token token :iss :team-invitation})) @@ -122,14 +121,13 @@ (assoc profile :is-admin (let [admins (cf/get :admins)] (contains? admins (:email profile)))))] (-> response - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/props (audit/profile->props profile) ::audit/profile-id (:id profile)})))))) -(s/def ::scope ::us/string) (s/def ::login-with-password (s/keys :req-un [::email ::password] - :opt-un [::invitation-token ::scope])) + :opt-un [::invitation-token])) (sv/defmethod ::login-with-password "Performs authentication using penpot password." @@ -148,8 +146,8 @@ "Clears the authentication cookie and logout the current session." {::rpc/auth false ::doc/added "1.15"} - [{:keys [session] :as cfg} _] - (rph/with-transform {} (session/delete-fn session))) + [cfg _] + (rph/with-transform {} (session/delete-fn cfg))) ;; ---- COMMAND: Recover Profile @@ -226,7 +224,7 @@ (validate-register-attempt! cfg params) - (let [profile (when-let [profile (profile/retrieve-profile-data-by-email pool (:email params))] + (let [profile (when-let [profile (profile/get-profile-by-email pool (:email params))] (cond (:is-blocked profile) (ex/raise :type :restriction @@ -267,10 +265,11 @@ ;; ---- COMMAND: Register Profile -(defn create-profile +(defn create-profile! "Create the profile entry on the database with limited set of input attrs (all the other attrs are filled with default values)." - [conn params] + [conn {:keys [email] :as params}] + (us/assert! ::us/email email) (let [id (or (:id params) (uuid/next)) props (-> (audit/extract-utm-params params) (merge (:props params)) @@ -291,7 +290,7 @@ is-demo (:is-demo params false) is-muted (:is-muted params false) is-active (:is-active params false) - email (str/lower (:email params)) + email (str/lower email) params {:id id :fullname (:fullname params) @@ -306,7 +305,7 @@ :is-demo is-demo}] (try (-> (db/insert! conn :profile params) - (profile/decode-profile-row)) + (profile/decode-row)) (catch org.postgresql.util.PSQLException e (let [state (.getSQLState e)] (if (not= state "23505") @@ -315,15 +314,17 @@ :code :email-already-exists :cause e))))))) -(defn create-profile-relations - [conn profile] - (let [team (teams/create-team conn {:profile-id (:id profile) +(defn create-profile-rels! + [conn {:keys [id] :as profile}] + (let [team (teams/create-team conn {:profile-id id :name "Default" :is-default true})] - (-> profile - (profile/strip-private-attrs) - (assoc :default-team-id (:id team)) - (assoc :default-project-id (:default-project-id team))))) + (-> (db/update! conn :profile + {:default-team-id (:id team) + :default-project-id (:default-project-id team)} + {:id id}) + (profile/decode-row)))) + (defn send-email-verification! [conn props profile] @@ -347,22 +348,18 @@ :extra-data ptoken}))) (defn register-profile - [{:keys [conn session] :as cfg} {:keys [token] :as params}] + [{:keys [conn] :as cfg} {:keys [token] :as params}] (let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register}) params (merge params claims) is-active (or (:is-active params) - (not (contains? cf/flags :email-verification)) - - ;; DEPRECATED: v1.15 - (contains? cf/flags :insecure-register)) + (not (contains? cf/flags :email-verification))) profile (if-let [profile-id (:profile-id claims)] - (profile/retrieve-profile conn profile-id) - (->> (assoc params :is-active is-active) - (create-profile conn) - (create-profile-relations conn) - (profile/decode-profile-row))) + (profile/get-profile conn profile-id) + (->> (create-profile! conn (assoc params :is-active is-active)) + (create-profile-rels! conn))) + invitation (when-let [token (:invitation-token params)] (tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))] @@ -389,7 +386,7 @@ token (tokens/generate (::main/props cfg) claims) resp {:invitation-token token}] (-> resp - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)}))) @@ -398,7 +395,7 @@ ;; we need to mark this session as logged. (not= "penpot" (:auth-backend profile)) (-> (profile/strip-private-attrs profile) - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)})) @@ -406,7 +403,7 @@ ;; to sign in the user directly, without email verification. (true? is-active) (-> (profile/strip-private-attrs profile) - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)})) @@ -448,7 +445,7 @@ :exp (dt/in-future {:days 30})})] (eml/send! {::eml/conn conn ::eml/factory eml/password-recovery - :public-uri (:public-uri cfg) + :public-uri (cf/get :public-uri) :to (:email profile) :token (:token profile) :name (:fullname profile) @@ -456,7 +453,7 @@ nil))] (db/with-atomic [conn pool] - (when-let [profile (profile/retrieve-profile-data-by-email conn email)] + (when-let [profile (profile/get-profile-by-email conn email)] (when-not (eml/allow-send-emails? conn profile) (ex/raise :type :validation :code :profile-is-muted diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index bd7eb1b61..22d994441 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -436,9 +436,8 @@ (s/def ::embed-assets? (s/nilable ::us/boolean)) (s/def ::write-export-options - (s/keys :req-un [::db/pool ::sto/storage] - :req [::output ::file-ids] - :opt [::include-libraries? ::embed-assets?])) + (s/keys :req [::db/pool ::sto/storage ::output ::file-ids] + :opt [::include-libraries? ::embed-assets?])) (defn write-export! "Do the exportation of a specified file in custom penpot binary @@ -555,9 +554,8 @@ (s/def ::ignore-index-errors? (s/nilable ::us/boolean)) (s/def ::read-import-options - (s/keys :req-un [::db/pool ::sto/storage] - :req [::project-id ::input] - :opt [::overwrite? ::migrate? ::ignore-index-errors?])) + (s/keys :req [::db/pool ::sto/storage ::project-id ::input] + :opt [::overwrite? ::migrate? ::ignore-index-errors?])) (defn read-import! "Do the importation of the specified resource in penpot custom binary @@ -580,7 +578,7 @@ (read-import (assoc options ::version version ::timestamp timestamp)))) (defmethod read-import :v1 - [{:keys [pool ::input] :as options}] + [{:keys [::db/pool ::input] :as options}] (with-open [input (zstd-input-stream input)] (with-open [input (io/data-input-stream input)] (db/with-atomic [conn pool] @@ -673,7 +671,7 @@ (db/insert! conn :file-library-rel rel))))) (defmethod read-section :v1/sobjects - [{:keys [storage conn ::input ::overwrite?]}] + [{:keys [::sto/storage conn ::input ::overwrite?]}] (let [storage (media/configure-assets-storage storage) ids (read-obj! input)] @@ -871,13 +869,14 @@ (s/def ::embed-assets? ::us/boolean) (s/def ::export-binfile - (s/keys :req [::rpc/profile-id] :req-un [::file-id ::include-libraries? ::embed-assets?])) + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::include-libraries? ::embed-assets?])) (sv/defmethod ::export-binfile "Export a penpot file in a binary format." {::doc/added "1.15" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}] (files/check-read-permissions! pool profile-id file-id) (let [body (reify yrs/StreamableResponseBody (-write-body-to-stream [_ _ output-stream] @@ -892,7 +891,8 @@ (s/def ::file ::media/upload) (s/def ::import-binfile - (s/keys :req [::rpc/profile-id] :req-un [::project-id ::file])) + (s/keys :req [::rpc/profile-id] + :req-un [::project-id ::file])) (sv/defmethod ::import-binfile "Import a penpot file in a binary format." diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index 73e40642f..f75dbf3b8 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -54,8 +54,8 @@ :hint "file not found")))) (defn- get-comment-thread - [conn thread-id & {:keys [for-update?]}] - (-> (db/get-by-id conn :comment-thread thread-id {:for-update for-update?}) + [conn thread-id & {:as opts}] + (-> (db/get-by-id conn :comment-thread thread-id opts) (decode-row))) (defn- get-comment @@ -374,7 +374,7 @@ {::doc/added "1.15"} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (upsert-comment-thread-status! conn profile-id id)))) @@ -391,7 +391,7 @@ {::doc/added "1.15"} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (db/update! conn :comment-thread {:is-resolved is-resolved} @@ -414,7 +414,7 @@ ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id :for-update? true) + (let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true) {:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)] (files/check-comment-permissions! conn profile-id (:id file) share-id) @@ -467,8 +467,8 @@ {::doc/added "1.15"} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}] (db/with-atomic [conn pool] - (let [{:keys [thread-id] :as comment} (get-comment conn id :for-update? true) - {:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id :for-update? true)] + (let [{:keys [thread-id] :as comment} (get-comment conn id ::db/for-update? true) + {:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) @@ -500,7 +500,7 @@ {::doc/added "1.15"} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (when-not (= owner-id profile-id) (ex/raise :type :validation @@ -520,7 +520,7 @@ {::doc/added "1.15"} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [owner-id thread-id] :as comment} (get-comment conn id :for-update? true) + (let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::db/for-update? true) {:keys [file-id] :as thread} (get-comment-thread conn thread-id)] (files/check-comment-permissions! conn profile-id file-id share-id) (when-not (= owner-id profile-id) @@ -540,7 +540,7 @@ {::doc/added "1.15"} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (db/update! conn :comment-thread {:modified-at (::rpc/request-at params) @@ -560,7 +560,7 @@ {::doc/added "1.15"} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}] (db/with-atomic [conn pool] - (let [{:keys [file-id] :as thread} (get-comment-thread conn id :for-update? true)] + (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) (db/update! conn :comment-thread {:modified-at (::rpc/request-at params) diff --git a/backend/src/app/rpc/commands/demo.clj b/backend/src/app/rpc/commands/demo.clj index bcc3d1d6e..1e1fd6bce 100644 --- a/backend/src/app/rpc/commands/demo.clj +++ b/backend/src/app/rpc/commands/demo.clj @@ -8,12 +8,11 @@ "A demo specific mutations." (:require [app.common.exceptions :as ex] - [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.loggers.audit :as audit] [app.rpc :as-alias rpc] - [app.rpc.commands.auth :as cmd.auth] + [app.rpc.commands.auth :as auth] [app.rpc.doc :as-alias doc] [app.util.services :as sv] [app.util.time :as dt] @@ -31,31 +30,30 @@ ::doc/added "1.15" ::doc/changes ["1.15" "This method is migrated from mutations to commands."]} [{:keys [pool] :as cfg} _] - (let [id (uuid/next) - sem (System/currentTimeMillis) + + (when-not (contains? cf/flags :demo-users) + (ex/raise :type :validation + :code :demo-users-not-allowed + :hint "Demo users are disabled by config.")) + + (let [sem (System/currentTimeMillis) email (str "demo-" sem ".demo@example.com") fullname (str "Demo User " sem) + password (-> (bn/random-bytes 16) (bc/bytes->b64u) (bc/bytes->str)) - params {:id id - :email email + + params {:email email :fullname fullname :is-active true :deleted-at (dt/in-future cf/deletion-delay) :password password - :props {} - }] - - (when-not (contains? cf/flags :demo-users) - (ex/raise :type :validation - :code :demo-users-not-allowed - :hint "Demo users are disabled by config.")) + :props {}}] (db/with-atomic [conn pool] - (->> (cmd.auth/create-profile conn params) - (cmd.auth/create-profile-relations conn)) - - (with-meta {:email email - :password password} - {::audit/profile-id id})))) + (let [profile (->> (auth/create-profile! conn params) + (auth/create-profile-rels! conn))] + (with-meta {:email email + :password password} + {::audit/profile-id (:id profile)}))))) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 5d6895c72..b05e50bd9 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -189,7 +189,7 @@ (let [row (db/get conn :file-data-fragment {:id id :file-id file-id} {:columns [:content] - :check-deleted? false})] + ::db/check-deleted? false})] (blob/decode (:content row)))) (defn persist-pointers! @@ -811,7 +811,7 @@ (let [ldata (-> library decode-row pmg/migrate-file :data)] (->> (db/query conn :file-library-rel {:library-file-id id}) (map :file-id) - (keep #(db/get-by-id conn :file % {:check-deleted? false})) + (keep #(db/get-by-id conn :file % ::db/check-deleted? false)) (map decode-row) (map pmg/migrate-file) (run! (fn [{:keys [id data revn] :as file}] diff --git a/backend/src/app/rpc/commands/files/temp.clj b/backend/src/app/rpc/commands/files/temp.clj index 0bc2c1c87..193bb285a 100644 --- a/backend/src/app/rpc/commands/files/temp.clj +++ b/backend/src/app/rpc/commands/files/temp.clj @@ -45,7 +45,7 @@ ;; --- MUTATION COMMAND: update-temp-file (defn update-temp-file - [conn {:keys [::rpc/profile-id session-id id revn changes] :as params}] + [conn {:keys [profile-id session-id id revn changes] :as params}] (db/insert! conn :file-change {:id (uuid/next) :session-id session-id @@ -57,16 +57,17 @@ :changes (blob/encode changes)})) (s/def ::update-temp-file - (s/keys :req-un [::files.update/changes + (s/keys :req [::rpc/profile-id] + :req-un [::files.update/changes ::files.update/revn ::files.update/session-id ::files/id])) (sv/defmethod ::update-temp-file {::doc/added "1.17"} - [{:keys [pool] :as cfg} params] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] - (update-temp-file conn params) + (update-temp-file conn (assoc params :profile-id profile-id)) nil)) ;; --- MUTATION COMMAND: persist-temp-file diff --git a/backend/src/app/rpc/commands/files/update.clj b/backend/src/app/rpc/commands/files/update.clj index 79cf1d759..14330e4c1 100644 --- a/backend/src/app/rpc/commands/files/update.clj +++ b/backend/src/app/rpc/commands/files/update.clj @@ -145,7 +145,7 @@ (l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))) (defn update-file - [{:keys [conn metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}] + [{:keys [conn ::mtx/metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}] (let [file (get-file conn id) features (->> (concat (:features file) (:features params)) @@ -275,7 +275,7 @@ (defn- send-notifications! [{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}] (let [lchanges (filter library-change? changes) - msgbus (:msgbus cfg)] + msgbus (::mbus/msgbus cfg)] ;; Asynchronously publish message to the msgbus (mbus/pub! msgbus diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index 6283e1423..e4367769e 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -14,7 +14,7 @@ [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.commands.auth :as auth] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.queries.profile :as profile] @@ -39,7 +39,7 @@ is properly configured and enabled with `login-with-ldap` flag." {::rpc/auth false ::doc/added "1.15"} - [{:keys [::main/props ::ldap/provider session] :as cfg} params] + [{:keys [::main/props ::ldap/provider] :as cfg} params] (when-not provider (ex/raise :type :restriction :code :ldap-not-initialized @@ -67,12 +67,12 @@ :member-email (:email profile)) token (tokens/generate props claims)] (-> {:invitation-token token} - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/props (:props profile) ::audit/profile-id (:id profile)}))) (-> profile - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/props (:props profile) ::audit/profile-id (:id profile)})))))) @@ -80,11 +80,10 @@ [{:keys [pool] :as cfg} info] (db/with-atomic [conn pool] (or (some->> (:email info) - (profile/retrieve-profile-data-by-email conn) - (profile/populate-additional-data conn) - (profile/decode-profile-row)) + (profile/get-profile-by-email conn) + (profile/decode-row)) (->> (assoc info :is-active true :is-demo false) - (cmd.auth/create-profile conn) - (cmd.auth/create-profile-relations conn) + (auth/create-profile! conn) + (auth/create-profile-rels! conn) (profile/strip-private-attrs))))) diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index e22a7bb85..f7853baa4 100644 --- a/backend/src/app/rpc/commands/media.clj +++ b/backend/src/app/rpc/commands/media.clj @@ -23,6 +23,7 @@ [app.storage.tmp :as tmp] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as-alias wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.io :as io] @@ -66,8 +67,8 @@ (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)] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) (media/validate-media-type! content) (validate-content-size! content) @@ -110,7 +111,7 @@ ;; inverse, soft referential integrity). (defn create-file-media-object - [{:keys [storage pool climit executor]} + [{:keys [::sto/storage ::db/pool climit ::wrk/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 @@ -186,8 +187,8 @@ (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)] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) (create-file-media-object-from-url cfg params))) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index bf2346f2f..24a490564 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -28,6 +28,7 @@ [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as-alias wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] [promesa.core :as p] @@ -114,8 +115,8 @@ (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]) + (let [profile (profile/get-profile conn profile-id)] + (->> (db/exec! conn [sql:teams (:default-team-id profile) profile-id]) (mapv process-permissions)))) ;; --- Query: Team (by ID) @@ -134,14 +135,15 @@ (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])] + (let [profile (profile/get-profile conn profile-id) + sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?") + result (db/exec-one! conn [sql (:default-team-id profile) profile-id team-id])] + (when-not result (ex/raise :type :not-found :code :team-does-not-exist)) - (process-permissions result))) + (process-permissions result))) ;; --- Query: Team Members @@ -583,11 +585,11 @@ [cfg {:keys [::rpc/profile-id 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)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (update-team-photo cfg (assoc params :profile-id profile-id)))) (defn update-team-photo - [{:keys [pool storage executor] :as cfg} {:keys [profile-id team-id] :as params}] + [{:keys [::db/pool ::sto/storage ::wrk/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)] @@ -605,7 +607,7 @@ (assoc team :photo-id (:id photo)))) (defn upload-photo - [{:keys [storage executor climit] :as cfg} {:keys [file]}] + [{:keys [::sto/storage ::wrk/executor climit] :as cfg} {:keys [file]}] (letfn [(get-info [content] (climit/with-dispatch (:process-image climit) (media/run {:cmd :info :input content}))) @@ -663,7 +665,7 @@ (defn- create-invitation [{:keys [::conn] :as cfg} {:keys [team profile role email] :as params}] - (let [member (profile/retrieve-profile-data-by-email conn email) + (let [member (profile/get-profile-by-email conn email) expire (dt/in-future "168h") ;; 7 days itoken (create-invitation-token cfg {:profile-id (:id profile) :valid-until expire @@ -838,7 +840,7 @@ {:team-id team-id :email-to (str/lower email)}) (update :role keyword)) - member (profile/retrieve-profile-data-by-email pool (:email invit)) + member (profile/get-profile-by-email pool (:email invit)) token (create-invitation-token cfg {:team-id (:team-id invit) :profile-id profile-id :valid-until (:valid-until invit) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 6eb455f22..ba9f6b98a 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -11,6 +11,7 @@ [app.db :as db] [app.http.session :as session] [app.loggers.audit :as audit] + [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] @@ -34,15 +35,15 @@ (sv/defmethod ::verify-token {::rpc/auth false ::doc/added "1.15"} - [{:keys [pool sprops] :as cfg} {:keys [token] :as params}] + [{:keys [pool] :as cfg} {:keys [token] :as params}] (db/with-atomic [conn pool] - (let [claims (tokens/verify sprops {:token token}) + (let [claims (tokens/verify (::main/props cfg) {:token token}) cfg (assoc cfg :conn conn)] (process-token cfg params claims)))) (defmethod process-token :change-email [{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}] - (when (profile/retrieve-profile-data-by-email conn email) + (when (profile/get-profile-by-email conn email) (ex/raise :type :validation :code :email-already-exists)) @@ -56,8 +57,8 @@ ::audit/profile-id profile-id})) (defmethod process-token :verify-email - [{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}] - (let [profile (profile/retrieve-profile conn profile-id) + [{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}] + (let [profile (profile/get-profile conn profile-id) claims (assoc claims :profile profile)] (when-not (:is-active profile) @@ -71,14 +72,14 @@ {:id (:id profile)})) (-> claims - (rph/with-transform (session/create-fn session profile-id)) + (rph/with-transform (session/create-fn cfg profile-id)) (rph/with-meta {::audit/name "verify-profile-email" ::audit/props (audit/profile->props profile) ::audit/profile-id (:id profile)})))) (defmethod process-token :auth [{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}] - (let [profile (profile/retrieve-profile conn profile-id)] + (let [profile (profile/get-profile conn profile-id)] (assoc claims :profile profile))) ;; --- Team Invitation @@ -133,7 +134,7 @@ :opt-un [::spec.team-invitation/member-id])) (defmethod process-token :team-invitation - [{:keys [conn session] :as cfg} + [{:keys [conn] :as cfg} {:keys [::rpc/profile-id token]} {:keys [member-id team-id member-email] :as claims}] @@ -179,7 +180,7 @@ {:columns [:id :email]})] (let [profile (accept-invitation cfg claims invitation member)] (-> (assoc claims :state :created) - (rph/with-transform (session/create-fn session (:id profile))) + (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/name "accept-team-invitation" ::audit/props (merge (audit/profile->props profile) diff --git a/backend/src/app/rpc/doc.clj b/backend/src/app/rpc/doc.clj index 112ecfad0..32889c9b7 100644 --- a/backend/src/app/rpc/doc.clj +++ b/backend/src/app/rpc/doc.clj @@ -70,6 +70,8 @@ (respond (yrs/response 404))))) +(s/def ::routes vector?) + (defmethod ig/pre-init-spec ::routes [_] (s/keys :req-un [::rpc/methods])) diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj index e354074f8..fa4d70069 100644 --- a/backend/src/app/rpc/mutations/fonts.clj +++ b/backend/src/app/rpc/mutations/fonts.clj @@ -22,6 +22,7 @@ [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as-alias wrk] [clojure.spec.alpha :as s] [promesa.core :as p] [promesa.exec :as px])) @@ -48,7 +49,7 @@ {::doc/added "1.3" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}] - (let [cfg (update cfg :storage media/configure-assets-storage)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (teams/check-edition-permissions! pool profile-id team-id) (quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team ::quotes/profile-id profile-id @@ -56,7 +57,7 @@ (create-font-variant cfg params))) (defn create-font-variant - [{:keys [storage pool executor climit] :as cfg} {:keys [data] :as params}] + [{:keys [::sto/storage ::db/pool ::wrk/executor climit] :as cfg} {:keys [data] :as params}] (letfn [(generate-fonts [data] (climit/with-dispatch (:process-font climit) (media/run {:cmd :generate-fonts :input data}))) diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index f66739549..964cf37f8 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -11,6 +11,7 @@ [app.rpc.commands.files :as files] [app.rpc.commands.media :as cmd.media] [app.rpc.doc :as-alias doc] + [app.storage :as-alias sto] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -22,7 +23,7 @@ {::doc/added "1.2" ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}] - (let [cfg (update cfg :storage media/configure-assets-storage)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) (media/validate-media-type! content) (cmd.media/validate-content-size! content) @@ -36,7 +37,7 @@ {::doc/added "1.3" ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (let [cfg (update cfg :storage media/configure-assets-storage)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) (#'cmd.media/create-file-media-object-from-url cfg params))) diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 8112bb661..4c70f4cfc 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -15,6 +15,7 @@ [app.emails :as eml] [app.http.session :as session] [app.loggers.audit :as audit] + [app.main :as-alias main] [app.media :as media] [app.rpc :as-alias rpc] [app.rpc.climit :as-alias climit] @@ -27,6 +28,7 @@ [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as-alias wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] [promesa.core :as p] @@ -51,13 +53,13 @@ (sv/defmethod ::update-profile {::doc/added "1.0"} - [{:keys [pool] :as cfg} {:keys [profile-id fullname lang theme] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [profile-id fullname lang theme] :as params}] (db/with-atomic [conn pool] ;; NOTE: we need to retrieve the profile independently if we use ;; it or not for explicit locking and avoid concurrent updates of ;; the same row/object. - (let [profile (-> (db/get-by-id conn :profile profile-id {:for-update true}) - (profile/decode-profile-row)) + (let [profile (-> (db/get-by-id conn :profile profile-id ::db/for-update? true) + (profile/decode-row)) ;; Update the profile map with direct params profile (-> profile @@ -90,7 +92,7 @@ (sv/defmethod ::update-profile-password {::climit/queue :auth} - [{:keys [pool] :as cfg} {:keys [password] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [password] :as params}] (db/with-atomic [conn pool] (let [profile (validate-password! conn params) session-id (::rpc/session-id params)] @@ -135,11 +137,11 @@ [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)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (update-profile-photo cfg params))) (defn update-profile-photo - [{:keys [pool storage executor] :as cfg} {:keys [profile-id file] :as params}] + [{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id file] :as params}] (p/let [profile (px/with-dispatch executor (db/get-by-id pool :profile profile-id)) photo (teams/upload-photo cfg params)] @@ -169,7 +171,7 @@ (s/keys :req-un [::email])) (sv/defmethod ::request-email-change - [{:keys [pool] :as cfg} {:keys [profile-id email] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [profile-id email] :as params}] (db/with-atomic [conn pool] (let [profile (db/get-by-id conn :profile profile-id) cfg (assoc cfg :conn conn) @@ -190,13 +192,13 @@ {:changed true}) (defn- request-email-change - [{:keys [conn sprops] :as cfg} {:keys [profile email] :as params}] - (let [token (tokens/generate sprops + [{:keys [conn] :as cfg} {:keys [profile email] :as params}] + (let [token (tokens/generate (::main/props cfg) {:iss :change-email :exp (dt/in-future "15m") :profile-id (:id profile) :email email}) - ptoken (tokens/generate sprops + ptoken (tokens/generate (::main/props cfg) {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})] @@ -216,7 +218,7 @@ (eml/send! {::eml/conn conn ::eml/factory eml/change-email - :public-uri (:public-uri cfg) + :public-uri (cf/get :public-uri) :to (:email profile) :name (:fullname profile) :pending-email email @@ -225,11 +227,6 @@ nil)) -(defn select-profile-for-update - [conn id] - (db/get-by-id conn :profile id {:for-update true})) - - ;; --- MUTATION: Update Profile Props (s/def ::props map?) @@ -237,9 +234,9 @@ (s/keys :req-un [::profile-id ::props])) (sv/defmethod ::update-profile-props - [{:keys [pool] :as cfg} {:keys [profile-id props]}] + [{:keys [::db/pool] :as cfg} {:keys [profile-id props]}] (db/with-atomic [conn pool] - (let [profile (profile/retrieve-profile-data conn profile-id) + (let [profile (profile/get-profile conn profile-id ::db/for-update? true) props (reduce-kv (fn [props k v] ;; We don't accept namespaced keys (if (simple-ident? k) @@ -254,7 +251,7 @@ {:props (db/tjson props)} {:id profile-id}) - (profile/filter-profile-props props)))) + (profile/filter-props props)))) ;; --- MUTATION: Delete Profile @@ -267,7 +264,7 @@ (s/keys :req-un [::profile-id])) (sv/defmethod ::delete-profile - [{:keys [pool session] :as cfg} {:keys [profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [profile-id] :as params}] (db/with-atomic [conn pool] (let [teams (get-owned-teams-with-participants conn profile-id) deleted-at (dt/now)] @@ -290,7 +287,7 @@ {:deleted-at deleted-at} {:id profile-id}) - (rph/with-transform {} (session/delete-fn session))))) + (rph/with-transform {} (session/delete-fn cfg))))) (def sql:owned-teams "with owner_teams as ( diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj index 650ac1884..92945fbfa 100644 --- a/backend/src/app/rpc/mutations/teams.clj +++ b/backend/src/app/rpc/mutations/teams.clj @@ -15,6 +15,7 @@ [app.rpc.commands.teams :as cmd.teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] + [app.storage :as-alias sto] [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s] @@ -126,7 +127,7 @@ [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)] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (cmd.teams/update-team-photo cfg params))) ;; --- Mutation: Invite Member diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj index 1d5d605cc..6bfec336f 100644 --- a/backend/src/app/rpc/queries/profile.clj +++ b/backend/src/app/rpc/queries/profile.clj @@ -6,7 +6,6 @@ (ns app.rpc.queries.profile (:require - [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] @@ -17,8 +16,6 @@ ;; --- Helpers & Specs -(declare strip-private-attrs) - (s/def ::email ::us/email) (s/def ::fullname ::us/string) (s/def ::old-password ::us/string) @@ -30,73 +27,32 @@ ;; --- Query: Profile (own) -(declare retrieve-profile) -(declare retrieve-additional-data) +(declare decode-row) +(declare get-profile) +(declare strip-private-attrs) +(declare filter-props) (s/def ::profile (s/keys :opt-un [::profile-id])) (sv/defmethod ::profile {::rpc/auth false} - [{:keys [pool] :as cfg} {:keys [profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [profile-id]}] ;; We need to return the anonymous profile object in two cases, when ;; no profile-id is in session, and when db call raises not found. In all other ;; cases we need to reraise the exception. - (or (ex/try* - #(some->> profile-id (retrieve-profile pool)) - #(when (not= :not-found (:type (ex-data %))) (throw %))) - {:id uuid/zero - :fullname "Anonymous User"})) + (try + (-> (get-profile pool profile-id) + (strip-private-attrs) + (update :props filter-props)) + (catch Throwable _ + {:id uuid/zero :fullname "Anonymous User"}))) -(def ^:private sql:default-profile-team - "select t.id, name - from team as t - inner join team_profile_rel as tp on (tp.team_id = t.id) - where tp.profile_id = ? - and tp.is_owner is true - and t.is_default is true") - -(def ^:private sql:default-profile-project - "select p.id, name - from project as p - inner join project_profile_rel as tp on (tp.project_id = p.id) - where tp.profile_id = ? - and tp.is_owner is true - and p.is_default is true - and p.team_id = ?") - -(defn retrieve-additional-data - [conn id] - (let [team (db/exec-one! conn [sql:default-profile-team id]) - project (db/exec-one! conn [sql:default-profile-project id (:id team)])] - {:default-team-id (:id team) - :default-project-id (:id project)})) - -(defn populate-additional-data - [conn profile] - (merge profile (retrieve-additional-data conn (:id profile)))) - -(defn filter-profile-props - [props] - (into {} (filter (fn [[k _]] (simple-ident? k))) props)) - -(defn decode-profile-row - [{:keys [props] :as row}] - (cond-> row - (db/pgobject? props "jsonb") - (assoc :props (db/decode-transit-pgobject props)))) - -(defn retrieve-profile-data - [conn id] - (-> (db/get-by-id conn :profile id) - (decode-profile-row))) - -(defn retrieve-profile - [conn id] - (let [profile (->> (retrieve-profile-data conn id) - (strip-private-attrs) - (populate-additional-data conn))] - (update profile :props filter-profile-props))) +(defn get-profile + "Get profile by id. Throws not-found exception if no profile found." + [conn id & {:as attrs}] + (-> (db/get-by-id conn :profile id attrs) + (decode-row))) (def ^:private sql:profile-by-email "select p.* from profile as p @@ -104,14 +60,27 @@ and (p.deleted_at is null or p.deleted_at > now())") -(defn retrieve-profile-data-by-email +(defn get-profile-by-email + "Returns a profile looked up by email or `nil` if not match found." [conn email] - (ex/ignoring - (db/exec-one! conn [sql:profile-by-email (str/lower email)]))) + (->> (db/exec! conn [sql:profile-by-email (str/lower email)]) + (map decode-row) + (first))) -;; --- Attrs Helpers +;; --- HELPERS (defn strip-private-attrs "Only selects a publicly visible profile attrs." [row] (dissoc row :password :deleted-at)) + +(defn filter-props + "Removes all namespace qualified props from `props` attr." + [props] + (into {} (filter (fn [[k _]] (simple-ident? k))) props)) + +(defn decode-row + [{:keys [props] :as row}] + (cond-> row + (db/pgobject? props "jsonb") + (assoc :props (db/decode-transit-pgobject props)))) diff --git a/backend/src/app/rpc/queries/share_link.clj b/backend/src/app/rpc/queries/share_link.clj index 852d05cd1..8272e991f 100644 --- a/backend/src/app/rpc/queries/share_link.clj +++ b/backend/src/app/rpc/queries/share_link.clj @@ -16,8 +16,7 @@ (defn retrieve-share-link [conn file-id share-id] - (some-> (db/get-by-params conn :share-link - {:id share-id :file-id file-id} - {:check-not-found false}) + (some-> (db/get* conn :share-link + {:id share-id :file-id file-id}) (decode-share-link-row))) diff --git a/backend/src/app/rpc/quotes.clj b/backend/src/app/rpc/quotes.clj index 76cfc82f7..49e2bb71a 100644 --- a/backend/src/app/rpc/quotes.clj +++ b/backend/src/app/rpc/quotes.clj @@ -160,6 +160,28 @@ (assoc ::count-sql [sql:get-teams-per-profile profile-id]) (generic-check!))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; QUOTE: ACCESS-TOKENS-PER-PROFILE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private sql:get-access-tokens-per-profile + "select count(*) as total + from access_token + where profile_id = ?") + +(s/def ::access-tokens-per-profile + (s/keys :req [::profile-id ::target])) + +(defmethod check-quote ::access-tokens-per-profile + [{:keys [::profile-id ::target] :as quote}] + (us/assert! ::access-tokens-per-profile quote) + (-> quote + (assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE)) + (assoc ::quote-sql [sql:get-quotes-1 target profile-id]) + (assoc ::count-sql [sql:get-access-tokens-per-profile profile-id]) + (generic-check!))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; QUOTE: PROJECTS-PER-TEAM ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -279,7 +301,6 @@ (assoc ::count-sql [sql:get-files-per-project project-id]) (generic-check!))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; QUOTE: COMMENT-THREADS-PER-FILE ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/rpc/rlimit.clj b/backend/src/app/rpc/rlimit.clj index 5eb0f85ee..161d54c81 100644 --- a/backend/src/app/rpc/rlimit.clj +++ b/backend/src/app/rpc/rlimit.clj @@ -52,7 +52,7 @@ [app.config :as cf] [app.http :as-alias http] [app.loggers.audit :refer [parse-client-ip]] - [app.redis :as redis] + [app.redis :as rds] [app.redis.script :as-alias rscript] [app.rpc :as-alias rpc] [app.rpc.rlimit.result :as-alias lresult] @@ -71,7 +71,7 @@ (dt/duration 400)) (def ^:private default-options - {:codec redis/string-codec + {:codec rds/string-codec :timeout default-timeout}) (def ^:private bucket-rate-limit-script @@ -141,23 +141,23 @@ (let [script (-> bucket-rate-limit-script (assoc ::rscript/keys [(str key "." service "." user-id)]) (assoc ::rscript/vals (conj params (dt/->seconds now))))] - (-> (redis/eval! redis script) - (p/then (fn [result] - (let [allowed? (boolean (nth result 0)) - remaining (nth result 1) - reset (* (/ (inst-ms interval) rate) - (- capacity remaining))] - (l/trace :hint "limit processed" - :service service - :limit (name (::name limit)) - :strategy (name (::strategy limit)) - :opts (::opts limit) + (->> (rds/eval! redis script) + (p/fmap (fn [result] + (let [allowed? (boolean (nth result 0)) + remaining (nth result 1) + reset (* (/ (inst-ms interval) rate) + (- capacity remaining))] + (l/trace :hint "limit processed" + :service service + :limit (name (::name limit)) + :strategy (name (::strategy limit)) + :opts (::opts limit) :allowed? allowed? :remaining remaining) - (-> limit - (assoc ::lresult/allowed? allowed?) - (assoc ::lresult/reset (dt/plus now reset)) - (assoc ::lresult/remaining remaining)))))))) + (-> limit + (assoc ::lresult/allowed? allowed?) + (assoc ::lresult/reset (dt/plus now reset)) + (assoc ::lresult/remaining remaining)))))))) (defmethod process-limit :window [redis user-id now {:keys [::nreq ::unit ::key ::service] :as limit}] @@ -166,94 +166,113 @@ script (-> window-rate-limit-script (assoc ::rscript/keys [(str key "." service "." user-id "." (dt/format-instant ts))]) (assoc ::rscript/vals [nreq (dt/->seconds ttl)]))] - (-> (redis/eval! redis script) - (p/then (fn [result] - (let [allowed? (boolean (nth result 0)) - remaining (nth result 1)] - (l/trace :hint "limit processed" - :service service - :limit (name (::name limit)) - :strategy (name (::strategy limit)) - :opts (::opts limit) - :allowed? allowed? + (->> (rds/eval! redis script) + (p/fmap (fn [result] + (let [allowed? (boolean (nth result 0)) + remaining (nth result 1)] + (l/trace :hint "limit processed" + :service service + :limit (name (::name limit)) + :strategy (name (::strategy limit)) + :opts (::opts limit) + :allowed? allowed? :remaining remaining) - (-> limit - (assoc ::lresult/allowed? allowed?) - (assoc ::lresult/remaining remaining) - (assoc ::lresult/reset (dt/plus ts {unit 1}))))))))) + (-> limit + (assoc ::lresult/allowed? allowed?) + (assoc ::lresult/remaining remaining) + (assoc ::lresult/reset (dt/plus ts {unit 1}))))))))) -(defn- process-limits +(defn- process-limits! [redis user-id limits now] - (-> (p/all (map (partial process-limit redis user-id now) limits)) - (p/then (fn [results] - (let [remaining (->> results - (d/index-by ::name ::lresult/remaining) - (uri/map->query-string)) - reset (->> results - (d/index-by ::name (comp dt/->seconds ::lresult/reset)) - (uri/map->query-string)) - rejected (->> results - (filter (complement ::lresult/allowed?)) - (first))] + (->> (p/all (map (partial process-limit redis user-id now) limits)) + (p/fmap (fn [results] + (let [remaining (->> results + (d/index-by ::name ::lresult/remaining) + (uri/map->query-string)) + reset (->> results + (d/index-by ::name (comp dt/->seconds ::lresult/reset)) + (uri/map->query-string)) + rejected (->> results + (filter (complement ::lresult/allowed?)) + (first))] - (when rejected - (l/warn :hint "rejected rate limit" - :user-id (str user-id) - :limit-service (-> rejected ::service name) - :limit-name (-> rejected ::name name) - :limit-strategy (-> rejected ::strategy name))) + (when rejected + (l/warn :hint "rejected rate limit" + :user-id (str user-id) + :limit-service (-> rejected ::service name) + :limit-name (-> rejected ::name name) + :limit-strategy (-> rejected ::strategy name))) - {:enabled? true - :allowed? (not (some? rejected)) - :headers {"x-rate-limit-remaining" remaining - "x-rate-limit-reset" reset}}))))) + {:enabled? true + :allowed? (not (some? rejected)) + :headers {"x-rate-limit-remaining" remaining + "x-rate-limit-reset" reset}}))))) (defn- handle-response [f cfg params result] (if (:enabled? result) (let [headers (:headers result)] - (when-not (:allowed? result) - (ex/raise :type :rate-limit - :code :request-blocked - :hint "rate limit reached" - ::http/headers headers)) - (-> (f cfg params) - (p/then (fn [response] - (vary-meta response update ::http/headers merge headers))))) + (if (:allowed? result) + (->> (f cfg params) + (p/fmap (fn [response] + (vary-meta response update ::http/headers merge headers)))) + (p/rejected + (ex/error :type :rate-limit + :code :request-blocked + :hint "rate limit reached" + ::http/headers headers)))) (f cfg params))) +(defn- get-limits + [state skey sname] + (some->> (or (get-in @state [::limits skey]) + (get-in @state [::limits :default])) + (map #(assoc % ::service sname)) + (seq))) + +(defn- get-uid + [{:keys [::http/request] :as params}] + (or (::rpc/profile-id params) + (some-> request parse-client-ip) + uuid/zero)) + (defn wrap - [{:keys [rlimit redis] :as cfg} f mdata] + [{:keys [::rpc/rlimit ::rds/redis] :as cfg} f mdata] + (us/assert! ::rpc/rlimit rlimit) + (us/assert! ::rds/redis redis) + (if rlimit (let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name)) sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))] - (fn [cfg {:keys [::http/request] :as params}] - (let [uid (or (:profile-id params) - (some-> request parse-client-ip) - uuid/zero) - rsp (when (and uid @enabled?) - (when-let [limits (or (get-in @rlimit [::limits skey]) - (get-in @rlimit [::limits :default]))] - (let [redis (redis/get-or-connect redis ::rlimit default-options) - limits (map #(assoc % ::service sname) limits) - resp (-> (process-limits redis uid limits (dt/now)) - (p/catch (fn [cause] - ;; If we have an error on processing the rate-limit we just skip - ;; it for do not cause service interruption because of redis - ;; downtime or similar situation. - (l/error :hint "error on processing rate-limit" :cause cause) - {:enabled? false})))] + (fn [cfg params] + (if @enabled? + (try + (let [uid (get-uid params) + rsp (when-let [limits (get-limits rlimit skey sname)] + (let [redis (rds/get-or-connect redis ::rpc/rlimit default-options) + rsp (->> (process-limits! redis uid limits (dt/now)) + (p/merr (fn [cause] + ;; If we have an error on processing the rate-limit we just skip + ;; it for do not cause service interruption because of redis + ;; downtime or similar situation. + (l/error :hint "error on processing rate-limit" :cause cause) + (p/resolved {:enabled? false}))))] - ;; If soft rate are enabled, we process the rate-limit but return unprotected - ;; response. - (if (contains? cf/flags :soft-rpc-rlimit) - (p/resolved {:enabled? false}) - resp)))) + ;; If soft rate are enabled, we process the rate-limit but return unprotected + ;; response. + (if (contains? cf/flags :soft-rpc-rlimit) + {:enabled? false} + rsp)))] - rsp (or rsp (p/resolved {:enabled? false}))] + (->> (p/promise rsp) + (p/fmap #(or % {:enabled? false})) + (p/mcat #(handle-response f cfg params %)))) - (p/then rsp (partial handle-response f cfg params))))) + (catch Throwable cause + (p/rejected cause))) + + (f cfg params)))) f)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -289,8 +308,9 @@ (s/keys :req [::nreq ::unit])))) -(s/def ::rlimit - #(instance? clojure.lang.Agent %)) +(s/def ::rpc/rlimit + (s/nilable + #(instance? clojure.lang.Agent %))) (s/def ::config (s/map-of (s/or :kw keyword? :set set?) @@ -332,7 +352,7 @@ ::limits limits})))) (defn- refresh-config - [{:keys [state path executor scheduled-executor] :as params}] + [{:keys [::state ::path ::wrk/executor ::wrk/scheduled-executor] :as cfg}] (letfn [(update-config [{:keys [::updated-at] :as state}] (let [updated-at' (fs/last-modified-time path)] (merge state @@ -349,7 +369,7 @@ (schedule-next [state] (px/schedule! scheduled-executor (inst-ms (::refresh state)) - (partial refresh-config params)) + (partial refresh-config cfg)) state)] (send-via executor state update-config) @@ -371,10 +391,11 @@ (and (fs/exists? path) (fs/regular-file? path) path))) (defmethod ig/pre-init-spec :app.rpc/rlimit [_] - (s/keys :req-un [::wrk/executor ::wrk/scheduled-executor])) + (s/keys :req [::wrk/executor + ::wrk/scheduled-executor])) (defmethod ig/init-key ::rpc/rlimit - [_ {:keys [executor] :as params}] + [_ {:keys [::wrk/executor] :as cfg}] (when (contains? cf/flags :rpc-rlimit) (let [state (agent {})] (set-error-handler! state on-refresh-error) @@ -387,6 +408,6 @@ (send-via executor state (constantly {::refresh (dt/duration "5s")})) ;; Force a refresh - (refresh-config (assoc params :path path :state state))) + (refresh-config (assoc cfg ::path path ::state state))) state))) diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index 38c943a08..7f9f7ed23 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -70,7 +70,7 @@ [system & {:keys [update-fn id save? migrate? inc-revn?] :or {save? false migrate? true inc-revn? true}}] (db/with-atomic [conn (:app.db/pool system)] - (let [file (-> (db/get-by-id conn :file id {:for-update true}) + (let [file (-> (db/get-by-id conn :file id {::db/for-update? true}) (update :features db/decode-pgarray #{}))] (binding [*conn* conn pmap/*tracked* (atom {}) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index ae09aa2ff..1561626d8 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -71,7 +71,7 @@ (let [sprops (:app.setup/props system) pool (:app.db/pool system) - profile (profile/retrieve-profile-data-by-email pool email)] + profile (profile/get-profile-by-email pool email)] (cmd.auth/send-email-verification! pool sprops profile) :email-sent)) @@ -81,10 +81,9 @@ associated with the profile-id." [system email] (db/with-atomic [conn (:app.db/pool system)] - (when-let [profile (db/get-by-params conn :profile - {:email (str/lower email)} - {:columns [:id :email] - :check-not-found false})] + (when-let [profile (db/get* conn :profile + {:email (str/lower email)} + {:columns [:id :email]})] (when-not (:is-blocked profile) (db/update! conn :profile {:is-active true} {:id (:id profile)}) :activated)))) @@ -94,10 +93,9 @@ associated with the profile-id." [system email] (db/with-atomic [conn (:app.db/pool system)] - (when-let [profile (db/get-by-params conn :profile - {:email (str/lower email)} - {:columns [:id :email] - :check-not-found false})] + (when-let [profile (db/get* conn :profile + {:email (str/lower email)} + {:columns [:id :email]})] (when-not (:is-blocked profile) (db/update! conn :profile {:is-blocked true} {:id (:id profile)}) (db/delete! conn :http-session {:profile-id (:id profile)}) diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index 8de524c1a..9d0906cb6 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -188,7 +188,7 @@ res (db/update! (or conn pool) :storage-object {:touched-at (dt/now)} {:id id} - {:return-keys false})] + {::db/return-keys? false})] (pos? (:next.jdbc/update-count res))))) (defn get-object-data @@ -247,7 +247,7 @@ res (db/update! (or conn pool) :storage-object {:deleted-at (dt/now)} {:id id} - {:return-keys false})] + {::db/return-keys? false})] (pos? (:next.jdbc/update-count res))))) (dm/export impl/resolve-backend) diff --git a/backend/src/app/util/retry.clj b/backend/src/app/util/retry.clj index 666a09f47..cd3ae6d91 100644 --- a/backend/src/app/util/retry.clj +++ b/backend/src/app/util/retry.clj @@ -29,6 +29,6 @@ (throw cause#))))] (if (= ::retry result#) (do - (l/warn :hint "retrying operation" :label ~label) + (l/warn :hint "retrying operation" :label ~label :retry tnum#) (recur (inc tnum#))) result#)))) diff --git a/backend/src/app/util/websocket.clj b/backend/src/app/util/websocket.clj index 07dc5a52a..7b8ee1660 100644 --- a/backend/src/app/util/websocket.clj +++ b/backend/src/app/util/websocket.clj @@ -60,7 +60,7 @@ (assert (fn? on-snd-message) "'on-snd-message' should be a function") (assert (fn? on-connect) "'on-connect' should be a function") - (fn [{:keys [::yws/channel session-id] :as request}] + (fn [{:keys [::yws/channel] :as request}] (let [input-ch (a/chan input-buff-size) output-ch (a/chan output-buff-size) hbeat-ch (a/chan (a/sliding-buffer 6)) @@ -81,7 +81,6 @@ ::stop-ch stop-ch ::channel channel ::remote-addr ip-addr - ::http-session-id session-id ::user-agent uagent}) (atom)) diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj index 900988a7a..e1c5a298d 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -45,7 +45,7 @@ (defmethod ig/init-key ::executor [skey {:keys [::parallelism]}] - (let [prefix (if (vector? skey) (-> skey first name keyword) :default) + (let [prefix (if (vector? skey) (-> skey first name keyword) "default") tname (str "penpot/" prefix "/%s") factory (px/forkjoin-thread-factory :name tname)] (px/forkjoin-executor diff --git a/backend/test/backend_tests/bounce_handling_test.clj b/backend/test/backend_tests/bounce_handling_test.clj index e0c094969..2acb288ec 100644 --- a/backend/test/backend_tests/bounce_handling_test.clj +++ b/backend/test/backend_tests/bounce_handling_test.clj @@ -6,12 +6,12 @@ (ns backend-tests.bounce-handling-test (:require - [backend-tests.helpers :as th] [app.db :as db] [app.emails :as emails] [app.http.awsns :as awsns] [app.tokens :as tokens] [app.util.time :as dt] + [backend-tests.helpers :as th] [clojure.pprint :refer [pprint]] [clojure.test :as t] [mockery.core :refer [with-mocks]])) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index a14dd4374..4656f5f17 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -16,8 +16,10 @@ [app.config :as cf] [app.db :as db] [app.main :as main] + [app.media :as-alias mtx] [app.media] [app.migrations] + [app.msgbus :as-alias mbus] [app.rpc :as-alias rpc] [app.rpc.commands.auth :as cmd.auth] [app.rpc.commands.files :as files] @@ -64,52 +66,50 @@ (defn state-init [next] - (let [templates [{:id "test" - :name "test" - :file-uri "test" - :thumbnail-uri "test" - :path (-> "backend_tests/test_files/template.penpot" io/resource fs/path)}] - system (-> (merge main/system-config main/worker-config) - (assoc-in [:app.redis/redis :app.redis/uri] (:redis-uri config)) - (assoc-in [:app.db/pool :uri] (:database-uri config)) - (assoc-in [:app.db/pool :username] (:database-username config)) - (assoc-in [:app.db/pool :password] (:database-password config)) - (assoc-in [:app.rpc/methods :templates] templates) - (dissoc :app.srepl/server - :app.http/server - :app.http/router - :app.http.awsns/handler - :app.http.session/updater - :app.auth.oidc/google-provider - :app.auth.oidc/gitlab-provider - :app.auth.oidc/github-provider - :app.auth.oidc/generic-provider - :app.setup/builtin-templates - :app.auth.oidc/routes - :app.worker/executors-monitor - :app.http.oauth/handler - :app.notifications/handler - :app.loggers.sentry/reporter - :app.loggers.mattermost/reporter - :app.loggers.loki/reporter - :app.loggers.database/reporter - :app.loggers.zmq/receiver - :app.worker/cron - :app.worker/worker)) - _ (ig/load-namespaces system) - system (-> (ig/prep system) - (ig/init))] - (try - (binding [*system* system - *pool* (:app.db/pool system)] - (with-redefs [app.config/flags (flags/parse flags/default default-flags (:flags config)) - app.config/config config - app.loggers.audit/submit! (constantly nil) - app.auth/derive-password identity - app.auth/verify-password (fn [a b] {:valid (= a b)})] - (next))) - (finally - (ig/halt! system))))) + (with-redefs [app.config/flags (flags/parse flags/default default-flags) + app.config/config config + app.loggers.audit/submit! (constantly nil) + app.auth/derive-password identity + app.auth/verify-password (fn [a b] {:valid (= a b)})] + + (let [templates [{:id "test" + :name "test" + :file-uri "test" + :thumbnail-uri "test" + :path (-> "backend_tests/test_files/template.penpot" io/resource fs/path)}] + system (-> (merge main/system-config main/worker-config) + (assoc-in [:app.redis/redis :app.redis/uri] (:redis-uri config)) + (assoc-in [:app.db/pool :uri] (:database-uri config)) + (assoc-in [:app.db/pool :username] (:database-username config)) + (assoc-in [:app.db/pool :password] (:database-password config)) + (assoc-in [:app.rpc/methods :templates] templates) + (dissoc :app.srepl/server + :app.http/server + :app.http/router + :app.auth.oidc/google-provider + :app.auth.oidc/gitlab-provider + :app.auth.oidc/github-provider + :app.auth.oidc/generic-provider + :app.setup/builtin-templates + :app.auth.oidc/routes + :app.worker/executors-monitor + :app.http.oauth/handler + :app.notifications/handler + :app.loggers.mattermost/reporter + :app.loggers.loki/reporter + :app.loggers.database/reporter + :app.loggers.zmq/receiver + :app.worker/cron + :app.worker/worker)) + _ (ig/load-namespaces system) + system (-> (ig/prep system) + (ig/init))] + (try + (binding [*system* system + *pool* (:app.db/pool system)] + (next)) + (finally + (ig/halt! system)))))) (defn database-reset [next] @@ -163,8 +163,8 @@ params)] (with-open [conn (db/open pool)] (->> params - (cmd.auth/create-profile conn) - (cmd.auth/create-profile-relations conn)))))) + (cmd.auth/create-profile! conn) + (cmd.auth/create-profile-rels! conn)))))) (defn create-project* ([i params] (create-project* *pool* i params)) @@ -274,12 +274,10 @@ ([pool {:keys [file-id changes session-id profile-id revn] :or {session-id (uuid/next) revn 0}}] (with-open [conn (db/open pool)] - (let [msgbus (:app.msgbus/msgbus *system*) - metrics (:app.metrics/metrics *system*) - features #{"components/v2"}] - (files.update/update-file {:conn conn - :msgbus msgbus - :metrics metrics} + (let [features #{"components/v2"} + cfg (-> (select-keys *system* [::mbus/msgbus ::mtx/metrics]) + (assoc :conn conn))] + (files.update/update-file cfg {:id file-id :revn revn :features features diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 7e906dfbb..189f1e631 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -652,7 +652,9 @@ ;; check that the unknown frame thumbnail is deleted (let [res (th/db-exec! ["select * from file_object_thumbnail"])] (t/is (= 1 (count res))) - (t/is (= "new-data" (get-in res [0 :data]))))))) + (t/is (= "new-data" (get-in res [0 :data]))))) + + )) (t/deftest file-thumbnail-ops diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 56a67c029..75f83b7c3 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -150,7 +150,7 @@ (let [row (th/db-get :team {:id (:default-team-id prof)} - {:check-deleted? false})] + {::db/remove-deleted? false})] (t/is (dt/instant? (:deleted-at row)))) ;; query profile after delete diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index baef0d5f3..0e713976e 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -59,3 +59,10 @@ (.putLong buf (.getMostSignificantBits o)) (.putLong buf (.getLeastSignificantBits o)) (.array buf)))) + +#?(:clj + (defn from-bytes + [^bytes o] + (let [buf (ByteBuffer/wrap o)] + (UUID. ^long (.getLong buf) + ^long (.getLong buf))))) From 73a6f0a3470c1bb5600e963b6dc0055e3d0855f0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 13 Jan 2023 14:15:53 +0100 Subject: [PATCH 02/16] :paperclip: Update backend scripts/repl file --- backend/scripts/repl | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/backend/scripts/repl b/backend/scripts/repl index 3fe39461b..26bf66b88 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -2,7 +2,16 @@ export PENPOT_HOST=devenv export PENPOT_TENANT=dev -export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp enable-webhooks" +export PENPOT_FLAGS="\ + $PENPOT_FLAGS \ + enable-backend-asserts \ + enable-audit-log \ + enable-transit-readable-response \ + enable-demo-users \ + disable-secure-session-cookies \ + enable-smtp \ + enable-webhooks \ + enable-access-tokens"; # export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot" # export PENPOT_DATABASE_USERNAME="penpot" @@ -31,11 +40,12 @@ export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot export OPTIONS=" -A:jmx-remote -A:dev \ -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ - -J-Dlog4j2.configurationFile=log4j2-devenv.xml \ - -J-XX:+UseG1GC \ - -J-XX:-OmitStackTraceInFastThrow \ - -J-Xms50m -J-Xmx1024m \ -J-Djdk.attach.allowAttachSelf \ + -J-Dlog4j2.configurationFile=log4j2-devenv.xml \ + -J-Xms50m \ + -J-Xmx1024m \ + -J-XX:+UseZGC \ + -J-XX:-OmitStackTraceInFastThrow \ -J-XX:+UnlockDiagnosticVMOptions \ -J-XX:+DebugNonSafepoints"; From ecb757bcaf7cedfe8eb925f0824b7b308c6a1b93 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 13 Jan 2023 19:48:19 +0100 Subject: [PATCH 03/16] :tada: Move user feedback http handler to RPC command method --- backend/src/app/http/feedback.clj | 81 ------------------- backend/src/app/rpc.clj | 1 + backend/src/app/rpc/commands/feedback.clj | 56 +++++++++++++ frontend/src/app/main/repo.cljs | 9 --- .../src/app/main/ui/settings/feedback.cljs | 2 +- 5 files changed, 58 insertions(+), 91 deletions(-) delete mode 100644 backend/src/app/http/feedback.clj create mode 100644 backend/src/app/rpc/commands/feedback.clj diff --git a/backend/src/app/http/feedback.clj b/backend/src/app/http/feedback.clj deleted file mode 100644 index e602b533b..000000000 --- a/backend/src/app/http/feedback.clj +++ /dev/null @@ -1,81 +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.http.feedback - "A general purpose feedback module." - (:require - [app.common.data :as d] - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.config :as cf] - [app.db :as db] - [app.emails :as eml] - [app.http.session :as-alias session] - [app.rpc.queries.profile :as profile] - [app.worker :as wrk] - [clojure.spec.alpha :as s] - [integrant.core :as ig] - [promesa.core :as p] - [promesa.exec :as px] - [yetti.request :as yrq] - [yetti.response :as yrs])) - -(declare ^:private send-feedback) -(declare ^:private handler) - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::db/pool ::wrk/executor])) - -(defmethod ig/init-key ::handler - [_ {:keys [executor] :as cfg}] - (let [enabled? (contains? cf/flags :user-feedback)] - (if enabled? - (fn [request respond raise] - (-> (px/submit! executor #(handler cfg request)) - (p/then' respond) - (p/catch raise))) - (fn [_ _ raise] - (raise (ex/error :type :validation - :code :feedback-disabled - :hint "feedback module is disabled")))))) - -(defn- handler - [{:keys [pool] :as cfg} {:keys [::session/profile-id] :as request}] - (let [ftoken (cf/get :feedback-token ::no-token) - token (yrq/get-header request "x-feedback-token") - params (d/merge (:params request) - (:body-params request))] - (cond - (uuid? profile-id) - (let [profile (profile/retrieve-profile-data pool profile-id) - params (assoc params :from (:email profile))] - (send-feedback pool profile params)) - - (= token ftoken) - (send-feedback cfg nil params)) - - (yrs/response 204))) - -(s/def ::content ::us/string) -(s/def ::from ::us/email) -(s/def ::subject ::us/string) -(s/def ::feedback - (s/keys :req-un [::from ::subject ::content])) - -(defn- send-feedback - [pool profile params] - (let [params (us/conform ::feedback params) - destination (cf/get :feedback-destination)] - (eml/send! {::eml/conn pool - ::eml/factory eml/feedback - :from destination - :to destination - :profile profile - :reply-to (:from params) - :email (:from params) - :subject (:subject params) - :content (:content params)}) - nil)) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index a78601865..257637c25 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -328,6 +328,7 @@ 'app.rpc.commands.access-token 'app.rpc.commands.audit 'app.rpc.commands.auth + 'app.rpc.commands.feedback 'app.rpc.commands.binfile 'app.rpc.commands.comments 'app.rpc.commands.demo diff --git a/backend/src/app/rpc/commands/feedback.clj b/backend/src/app/rpc/commands/feedback.clj new file mode 100644 index 000000000..6f2f83f98 --- /dev/null +++ b/backend/src/app/rpc/commands/feedback.clj @@ -0,0 +1,56 @@ +;; 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.feedback + "A general purpose feedback module." + (:require + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.config :as cf] + [app.db :as db] + [app.emails :as eml] + [app.rpc :as-alias rpc] + [app.rpc.doc :as-alias doc] + [app.rpc.queries.profile :as profile] + [app.util.services :as sv] + [clojure.spec.alpha :as s])) + +(declare ^:private send-feedback!) + +(s/def ::content ::us/string) +(s/def ::from ::us/email) +(s/def ::subject ::us/string) + +(s/def ::send-user-feedback + (s/keys :req [::rpc/profile-id] + :req-un [::subject + ::content])) + +(sv/defmethod ::send-user-feedback + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}] + (when-not (contains? cf/flags :user-feedback) + (ex/raise :type :restriction + :code :feedback-disabled + :hint "feedback not enabled")) + + (let [profile (profile/get-profile pool profile-id)] + (send-feedback! pool profile params) + nil)) + +(defn- send-feedback! + [pool profile params] + (let [dest (cf/get :feedback-destination)] + (eml/send! {::eml/conn pool + ::eml/factory eml/feedback + :from dest + :to dest + :profile profile + :reply-to (:email profile) + :email (:email profile) + :subject (:subject params) + :content (:content params)}) + nil)) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 55c2e3976..b5b67fefc 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -164,15 +164,6 @@ (rx/map http/conditional-decode-transit) (rx/mapcat handle-response)))) -(defmethod command :send-feedback - [_ params] - (->> (http/send! {:method :post - :uri (u/join @cf/public-uri "api/feedback") - :credentials "include" - :body (http/transit-data params)}) - (rx/map http/conditional-decode-transit) - (rx/mapcat handle-response))) - (defn- send-export [{:keys [blob?] :as params}] (->> (http/send! {:method :post diff --git a/frontend/src/app/main/ui/settings/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index 10f1d88ac..e198088e6 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -55,7 +55,7 @@ (fn [form _] (reset! loading true) (let [data (:clean-data @form)] - (->> (rp/command! :send-feedback data) + (->> (rp/command! :send-user-feedback data) (rx/subs on-succes on-error)))))] [:& fm/form {:class "feedback-form" From d8faff47a830522662bbcef9d5086a5b792448b8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 14 Jan 2023 12:11:45 +0100 Subject: [PATCH 04/16] :recycle: Move profile queries and mutations to commands --- backend/src/app/auth/oidc.clj | 2 +- backend/src/app/cli/manage.clj | 5 +- backend/src/app/http/debug.clj | 2 +- backend/src/app/rpc.clj | 1 + backend/src/app/rpc/commands/auth.clj | 16 +- backend/src/app/rpc/commands/feedback.clj | 2 +- backend/src/app/rpc/commands/ldap.clj | 2 +- backend/src/app/rpc/commands/profile.clj | 424 ++++++++++++++++++ backend/src/app/rpc/commands/teams.clj | 37 +- backend/src/app/rpc/commands/verify_token.clj | 2 +- backend/src/app/rpc/mutations/profile.clj | 169 ++----- backend/src/app/rpc/queries/profile.clj | 74 +-- backend/src/app/srepl/main.clj | 6 +- .../test/backend_tests/rpc_profile_test.clj | 66 +-- frontend/src/app/main/data/users.cljs | 32 +- .../app/main/data/workspace/persistence.cljs | 2 +- frontend/src/app/main/repo.cljs | 10 + frontend/src/app/main/ui/routes.cljs | 2 +- 18 files changed, 537 insertions(+), 317 deletions(-) create mode 100644 backend/src/app/rpc/commands/profile.clj diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index 98c9c0e8f..d6030025a 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -21,7 +21,7 @@ [app.http.session :as session] [app.loggers.audit :as audit] [app.main :as-alias main] - [app.rpc.queries.profile :as profile] + [app.rpc.commands.profile :as profile] [app.tokens :as tokens] [app.util.json :as json] [app.util.time :as dt] diff --git a/backend/src/app/cli/manage.clj b/backend/src/app/cli/manage.clj index 8d992a210..a1799434a 100644 --- a/backend/src/app/cli/manage.clj +++ b/backend/src/app/cli/manage.clj @@ -11,8 +11,7 @@ [app.db :as db] [app.main :as main] [app.rpc.commands.auth :as auth] - [app.rpc.mutations.profile :as profile] - [app.rpc.queries.profile :refer [get-profile-by-email]] + [app.rpc.commands.profile :as profile] [clojure.string :as str] [clojure.tools.cli :refer [parse-opts]] [integrant.core :as ig]) @@ -80,7 +79,7 @@ (db/with-atomic [conn (:app.db/pool system)] (let [email (or (:email options) (read-from-console {:label "Email:"})) - profile (get-profile-by-email conn email)] + profile (profile/get-profile-by-email conn email)] (when-not profile (when (pos? (:verbosity options)) (println "Profile does not exists.")) diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 083ebc9a5..d2cbb6f7a 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -17,7 +17,7 @@ [app.http.session :as session] [app.rpc.commands.binfile :as binf] [app.rpc.commands.files.create :refer [create-file]] - [app.rpc.queries.profile :as profile] + [app.rpc.commands.profile :as profile] [app.util.blob :as blob] [app.util.template :as tmpl] [app.util.time :as dt] diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 257637c25..24fb1637d 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -131,6 +131,7 @@ data (-> params (assoc ::request-at (dt/now)) + (assoc ::session/id (::session/id request)) (assoc ::http/request request) (assoc ::cond/key etag) (cond-> (uuid? profile-id) diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 4a037b18b..17410c425 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -19,10 +19,10 @@ [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.climit :as climit] + [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.profile :as profile] [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] @@ -52,20 +52,6 @@ (str/split #"@" 2))] (contains? domains candidate)))) -(def ^:private sql:profile-existence - "select exists (select * from profile - where email = ? - and deleted_at is null) as val") - -(defn check-profile-existence! - [conn {:keys [email] :as params}] - (let [email (str/lower email) - result (db/exec-one! conn [sql:profile-existence email])] - (when (:val result) - (ex/raise :type :validation - :code :email-already-exists)) - params)) - ;; ---- COMMAND: login with password (defn login-with-password diff --git a/backend/src/app/rpc/commands/feedback.clj b/backend/src/app/rpc/commands/feedback.clj index 6f2f83f98..2f5c2c980 100644 --- a/backend/src/app/rpc/commands/feedback.clj +++ b/backend/src/app/rpc/commands/feedback.clj @@ -13,8 +13,8 @@ [app.db :as db] [app.emails :as eml] [app.rpc :as-alias rpc] + [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] - [app.rpc.queries.profile :as profile] [app.util.services :as sv] [clojure.spec.alpha :as s])) diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index e4367769e..921e7b4c7 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -15,9 +15,9 @@ [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.commands.auth :as auth] + [app.rpc.commands.profile :as profile] [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])) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj new file mode 100644 index 000000000..bb474ca08 --- /dev/null +++ b/backend/src/app/rpc/commands/profile.clj @@ -0,0 +1,424 @@ +;; 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.profile + (:require + [app.auth :as auth] + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.emails :as eml] + [app.http.session :as session] + [app.loggers.audit :as audit] + [app.main :as-alias main] + [app.media :as media] + [app.rpc :as-alias rpc] + [app.rpc.climit :as climit] + [app.rpc.doc :as-alias doc] + [app.rpc.helpers :as rph] + [app.storage :as sto] + [app.tokens :as tokens] + [app.util.services :as sv] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [promesa.core :as p] + [promesa.exec :as px])) + +(declare decode-row) +(declare get-profile) +(declare strip-private-attrs) +(declare filter-props) +(declare check-profile-existence!) + +;; --- QUERY: Get profile (own) + +(s/def ::get-profile + (s/keys :opt [::rpc/profile-id])) + +(sv/defmethod ::get-profile + {::rpc/auth false + ::doc/added "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id]}] + ;; We need to return the anonymous profile object in two cases, when + ;; no profile-id is in session, and when db call raises not found. In all other + ;; cases we need to reraise the exception. + (try + (-> (get-profile pool profile-id) + (strip-private-attrs) + (update :props filter-props)) + (catch Throwable _ + {:id uuid/zero :fullname "Anonymous User"}))) + +(defn get-profile + "Get profile by id. Throws not-found exception if no profile found." + [conn id & {:as attrs}] + (-> (db/get-by-id conn :profile id attrs) + (decode-row))) + + +;; --- MUTATION: Update Profile (own) + +(s/def ::email ::us/email) +(s/def ::fullname ::us/not-empty-string) +(s/def ::lang ::us/string) +(s/def ::theme ::us/string) + +(s/def ::update-profile + (s/keys :req [::rpc/profile-id] + :req-un [::fullname] + :opt-un [::lang ::theme])) + +(sv/defmethod ::update-profile + {::doc/added "1.0"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}] + (db/with-atomic [conn pool] + ;; NOTE: we need to retrieve the profile independently if we use + ;; it or not for explicit locking and avoid concurrent updates of + ;; the same row/object. + (let [profile (-> (db/get-by-id conn :profile profile-id ::db/for-update? true) + (decode-row)) + + ;; Update the profile map with direct params + profile (-> profile + (assoc :fullname fullname) + (assoc :lang lang) + (assoc :theme theme)) + ] + + (db/update! conn :profile + {:fullname fullname + :lang lang + :theme theme + :props (db/tjson (:props profile))} + {:id profile-id}) + + (-> profile + (strip-private-attrs) + (d/without-nils) + (rph/with-meta {::audit/props (audit/profile->props profile)}))))) + + +;; --- MUTATION: Update Password + +(declare validate-password!) +(declare update-profile-password!) +(declare invalidate-profile-session!) + +(s/def ::password ::us/not-empty-string) +(s/def ::old-password ::us/not-empty-string) + +(s/def ::update-profile-password + (s/keys :req [::rpc/profile-id] + :req-un [::password ::old-password])) + +(sv/defmethod ::update-profile-password + {::climit/queue :auth} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}] + (db/with-atomic [conn pool] + (let [profile (validate-password! conn (assoc params :profile-id profile-id)) + session-id (::session/id params)] + + (when (= (str/lower (:email profile)) + (str/lower (:password params))) + (ex/raise :type :validation + :code :email-as-password + :hint "you can't use your email as password")) + + (update-profile-password! conn (assoc profile :password password)) + (invalidate-profile-session! conn profile-id session-id) + nil))) + +(defn- invalidate-profile-session! + "Removes all sessions except the current one." + [conn profile-id session-id] + (let [sql "delete from http_session where profile_id = ? and id != ?"] + (:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id])))) + +(defn- validate-password! + [conn {:keys [profile-id old-password] :as params}] + (let [profile (db/get-by-id conn :profile profile-id ::db/for-update? true)] + (when-not (:valid (auth/verify-password old-password (:password profile))) + (ex/raise :type :validation + :code :old-password-not-match)) + profile)) + +(defn update-profile-password! + [conn {:keys [id password] :as profile}] + (db/update! conn :profile + {:password (auth/derive-password password)} + {:id id})) + +;; --- MUTATION: Update Photo + +(declare upload-photo) +(declare update-profile-photo) + +(s/def ::file ::media/upload) +(s/def ::update-profile-photo + (s/keys :req [::rpc/profile-id] + :req-un [::file])) + +(sv/defmethod ::update-profile-photo + [cfg {:keys [::rpc/profile-id file] :as params}] + ;; Validate incoming mime type + (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] + (update-profile-photo cfg (assoc params :profile-id profile-id)))) + +;; TODO: reimplement it without p/let + +(defn update-profile-photo + [{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id file] :as params}] + (letfn [(on-uploaded [photo] + (let [profile (db/get-by-id pool :profile profile-id ::db/for-update? true)] + + ;; Schedule deletion of old photo + (when-let [id (:photo-id profile)] + (sto/touch-object! storage id)) + + ;; Save new photo + (db/update! pool :profile + {:photo-id (:id photo)} + {:id profile-id}) + + (-> (rph/wrap) + (rph/with-meta {::audit/replace-props + {:file-name (:filename file) + :file-size (:size file) + :file-path (str (:path file)) + :file-mtype (:mtype file)}}))))] + (->> (upload-photo cfg params) + (p/fmap executor on-uploaded)))) + +(defn upload-photo + [{:keys [::sto/storage ::wrk/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: Request Email Change + +(declare ^:private request-email-change!) +(declare ^:private change-email-immediately!) + +(s/def ::request-email-change + (s/keys :req [::rpc/profile-id] + :req-un [::email])) + +(sv/defmethod ::request-email-change + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id email] :as params}] + (db/with-atomic [conn pool] + (let [profile (db/get-by-id conn :profile profile-id) + cfg (assoc cfg ::conn conn) + params (assoc params + :profile profile + :email (str/lower email))] + (if (contains? cf/flags :smtp) + (request-email-change! cfg params) + (change-email-immediately! cfg params))))) + +(defn- change-email-immediately! + [{:keys [::conn]} {:keys [profile email] :as params}] + (when (not= email (:email profile)) + (check-profile-existence! conn params)) + + (db/update! conn :profile + {:email email} + {:id (:id profile)}) + + {:changed true}) + +(defn- request-email-change! + [{:keys [::conn] :as cfg} {:keys [profile email] :as params}] + (let [token (tokens/generate (::main/props cfg) + {:iss :change-email + :exp (dt/in-future "15m") + :profile-id (:id profile) + :email email}) + ptoken (tokens/generate (::main/props cfg) + {:iss :profile-identity + :profile-id (:id profile) + :exp (dt/in-future {:days 30})})] + + (when (not= email (:email profile)) + (check-profile-existence! conn params)) + + (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.")) + + (when (eml/has-bounce-reports? conn email) + (ex/raise :type :validation + :code :email-has-permanent-bounces + :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) + + (eml/send! {::eml/conn conn + ::eml/factory eml/change-email + :public-uri (cf/get :public-uri) + :to (:email profile) + :name (:fullname profile) + :pending-email email + :token token + :extra-data ptoken}) + nil)) + + +;; --- MUTATION: Update Profile Props + +(s/def ::props map?) +(s/def ::update-profile-props + (s/keys :req [::rpc/profile-id] + :req-un [::props])) + +(sv/defmethod ::update-profile-props + [{:keys [::db/pool]} {:keys [::rpc/profile-id props]}] + (db/with-atomic [conn pool] + (let [profile (get-profile conn profile-id ::db/for-update? true) + props (reduce-kv (fn [props k v] + ;; We don't accept namespaced keys + (if (simple-ident? k) + (if (nil? v) + (dissoc props k) + (assoc props k v)) + props)) + (:props profile) + props)] + + (db/update! conn :profile + {:props (db/tjson props)} + {:id profile-id}) + + (filter-props props)))) + + +;; --- MUTATION: Delete Profile + +(declare ^:private get-owned-teams-with-participants) + +(s/def ::delete-profile + (s/keys :req [::rpc/profile-id])) + +(sv/defmethod ::delete-profile + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + (db/with-atomic [conn pool] + (let [teams (get-owned-teams-with-participants conn profile-id) + deleted-at (dt/now)] + + ;; If we found owned teams with participants, we don't allow + ;; delete profile until the user properly transfer ownership or + ;; explicitly removes all participants from the team + (when (some pos? (map :participants teams)) + (ex/raise :type :validation + :code :owner-teams-with-people + :hint "The user need to transfer ownership of owned teams." + :context {:teams (mapv :id teams)})) + + (doseq [{:keys [id]} teams] + (db/update! conn :team + {:deleted-at deleted-at} + {:id id})) + + (db/update! conn :profile + {:deleted-at deleted-at} + {:id profile-id}) + + (rph/with-transform {} (session/delete-fn cfg))))) + + +;; --- HELPERS + +(def sql:owned-teams + "with owner_teams as ( + select tpr.team_id as id + from team_profile_rel as tpr + where tpr.is_owner is true + and tpr.profile_id = ? + ) + select tpr.team_id as id, + count(tpr.profile_id) - 1 as participants + from team_profile_rel as tpr + where tpr.team_id in (select id from owner_teams) + and tpr.profile_id != ? + group by 1") + +(defn- get-owned-teams-with-participants + [conn profile-id] + (db/exec! conn [sql:owned-teams profile-id profile-id])) + +(def ^:private sql:profile-existence + "select exists (select * from profile + where email = ? + and deleted_at is null) as val") + +(defn check-profile-existence! + [conn {:keys [email] :as params}] + (let [email (str/lower email) + result (db/exec-one! conn [sql:profile-existence email])] + (when (:val result) + (ex/raise :type :validation + :code :email-already-exists)) + params)) + +(def ^:private sql:profile-by-email + "select p.* from profile as p + where p.email = ? + and (p.deleted_at is null or + p.deleted_at > now())") + +(defn get-profile-by-email + "Returns a profile looked up by email or `nil` if not match found." + [conn email] + (->> (db/exec! conn [sql:profile-by-email (str/lower email)]) + (map decode-row) + (first))) + +(defn strip-private-attrs + "Only selects a publicly visible profile attrs." + [row] + (dissoc row :password :deleted-at)) + +(defn filter-props + "Removes all namespace qualified props from `props` attr." + [props] + (into {} (filter (fn [[k _]] (simple-ident? k))) props)) + +(defn decode-row + [{:keys [props] :as row}] + (cond-> row + (db/pgobject? props "jsonb") + (assoc :props (db/decode-transit-pgobject props)))) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 24a490564..f70aa3d01 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -18,11 +18,10 @@ [app.main :as-alias main] [app.media :as media] [app.rpc :as-alias rpc] - [app.rpc.climit :as climit] + [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.permissions :as perms] - [app.rpc.queries.profile :as profile] [app.rpc.quotes :as quotes] [app.storage :as sto] [app.tokens :as tokens] @@ -572,7 +571,7 @@ ;; --- Mutation: Update Team Photo -(declare ^:private upload-photo) +(declare upload-photo) (declare ^:private update-team-photo) (s/def ::file ::media/upload) @@ -592,7 +591,7 @@ [{:keys [::db/pool ::sto/storage ::wrk/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)] + photo (profile/upload-photo cfg params)] ;; Mark object as touched for make it ellegible for tentative ;; garbage collection. @@ -606,36 +605,6 @@ (assoc team :photo-id (:id photo)))) -(defn upload-photo - [{:keys [::sto/storage ::wrk/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 diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index ba9f6b98a..914116ee9 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -13,10 +13,10 @@ [app.loggers.audit :as audit] [app.main :as-alias main] [app.rpc :as-alias rpc] + [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.profile :as profile] [app.rpc.quotes :as quotes] [app.tokens :as tokens] [app.tokens.spec.team-invitation :as-alias spec.team-invitation] diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 4c70f4cfc..406e029cb 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -6,33 +6,23 @@ (ns app.rpc.mutations.profile (:require - [app.auth :as auth] [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.config :as cf] [app.db :as db] - [app.emails :as eml] [app.http.session :as session] [app.loggers.audit :as audit] - [app.main :as-alias main] [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.teams :as teams] + [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.profile :as profile] [app.storage :as sto] - [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as-alias wrk] [clojure.spec.alpha :as s] - [cuerdas.core :as str] - [promesa.core :as p] - [promesa.exec :as px])) + [cuerdas.core :as str])) ;; --- Helpers & Specs @@ -52,7 +42,8 @@ :opt-un [::lang ::theme])) (sv/defmethod ::update-profile - {::doc/added "1.0"} + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [::db/pool] :as cfg} {:keys [profile-id fullname lang theme] :as params}] (db/with-atomic [conn pool] ;; NOTE: we need to retrieve the profile independently if we use @@ -76,156 +67,68 @@ {:id profile-id}) (-> profile - profile/strip-private-attrs - d/without-nils + (profile/strip-private-attrs) + (d/without-nils) (rph/with-meta {::audit/props (audit/profile->props profile)}))))) ;; --- MUTATION: Update Password -(declare validate-password!) -(declare update-profile-password!) -(declare invalidate-profile-session!) - (s/def ::update-profile-password (s/keys :req-un [::profile-id ::password ::old-password])) (sv/defmethod ::update-profile-password - {::climit/queue :auth} + {::climit/queue :auth + ::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [::db/pool] :as cfg} {:keys [password] :as params}] (db/with-atomic [conn pool] - (let [profile (validate-password! conn params) - session-id (::rpc/session-id params)] + (let [profile (#'profile/validate-password! conn params) + session-id (::session/id params)] (when (= (str/lower (:email profile)) (str/lower (:password params))) (ex/raise :type :validation :code :email-as-password :hint "you can't use your email as password")) - (update-profile-password! conn (assoc profile :password password)) - (invalidate-profile-session! conn (:id profile) session-id) + (profile/update-profile-password! conn (assoc profile :password password)) + (#'profile/invalidate-profile-session! conn (:id profile) session-id) nil))) -(defn- invalidate-profile-session! - "Removes all sessions except the current one." - [conn profile-id session-id] - (let [sql "delete from http_session where profile_id = ? and id != ?"] - (:next.jdbc/update-count (db/exec-one! conn [sql profile-id session-id])))) - -(defn- validate-password! - [conn {:keys [profile-id old-password] :as params}] - (let [profile (db/get-by-id conn :profile profile-id)] - (when-not (:valid (auth/verify-password old-password (:password profile))) - (ex/raise :type :validation - :code :old-password-not-match)) - profile)) - -(defn update-profile-password! - [conn {:keys [id password] :as profile}] - (db/update! conn :profile - {:password (auth/derive-password password)} - {:id id})) ;; --- MUTATION: Update Photo -(declare update-profile-photo) - (s/def ::file ::media/upload) (s/def ::update-profile-photo (s/keys :req-un [::profile-id ::file])) (sv/defmethod ::update-profile-photo + {::doc/added "1.0" + ::doc/deprecated "1.18"} [cfg {:keys [file] :as params}] ;; Validate incoming mime type (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] - (update-profile-photo cfg params))) - -(defn update-profile-photo - [{:keys [::db/pool ::sto/storage ::wrk/executor] :as cfg} {:keys [profile-id file] :as params}] - (p/let [profile (px/with-dispatch executor - (db/get-by-id pool :profile profile-id)) - photo (teams/upload-photo cfg params)] - - ;; Schedule deletion of old photo - (when-let [id (:photo-id profile)] - (sto/touch-object! storage id)) - - ;; Save new photo - (db/update! pool :profile - {:photo-id (:id photo)} - {:id profile-id}) - - (-> (rph/wrap) - (rph/with-meta {::audit/replace-props - {:file-name (:filename file) - :file-size (:size file) - :file-path (str (:path file)) - :file-mtype (:mtype file)}})))) + (profile/update-profile-photo cfg params))) ;; --- MUTATION: Request Email Change -(declare request-email-change) -(declare change-email-immediately) - (s/def ::request-email-change (s/keys :req-un [::email])) (sv/defmethod ::request-email-change + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [::db/pool] :as cfg} {:keys [profile-id email] :as params}] (db/with-atomic [conn pool] (let [profile (db/get-by-id conn :profile profile-id) - cfg (assoc cfg :conn conn) + cfg (assoc cfg ::profile/conn conn) params (assoc params :profile profile :email (str/lower email))] + (if (contains? cf/flags :smtp) - (request-email-change cfg params) - (change-email-immediately cfg params))))) - -(defn- change-email-immediately - [{:keys [conn]} {:keys [profile email] :as params}] - (when (not= email (:email profile)) - (cmd.auth/check-profile-existence! conn params)) - (db/update! conn :profile - {:email email} - {:id (:id profile)}) - {:changed true}) - -(defn- request-email-change - [{:keys [conn] :as cfg} {:keys [profile email] :as params}] - (let [token (tokens/generate (::main/props cfg) - {:iss :change-email - :exp (dt/in-future "15m") - :profile-id (:id profile) - :email email}) - ptoken (tokens/generate (::main/props cfg) - {:iss :profile-identity - :profile-id (:id profile) - :exp (dt/in-future {:days 30})})] - - (when (not= email (:email profile)) - (cmd.auth/check-profile-existence! conn params)) - - (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.")) - - (when (eml/has-bounce-reports? conn email) - (ex/raise :type :validation - :code :email-has-permanent-bounces - :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) - - (eml/send! {::eml/conn conn - ::eml/factory eml/change-email - :public-uri (cf/get :public-uri) - :to (:email profile) - :name (:fullname profile) - :pending-email email - :token token - :extra-data ptoken}) - nil)) - + (#'profile/request-email-change! cfg params) + (#'profile/change-email-immediately! cfg params))))) ;; --- MUTATION: Update Profile Props @@ -234,6 +137,8 @@ (s/keys :req-un [::profile-id ::props])) (sv/defmethod ::update-profile-props + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [::db/pool] :as cfg} {:keys [profile-id props]}] (db/with-atomic [conn pool] (let [profile (profile/get-profile conn profile-id ::db/for-update? true) @@ -256,17 +161,15 @@ ;; --- MUTATION: Delete Profile -(declare get-owned-teams-with-participants) -(declare check-can-delete-profile!) -(declare mark-profile-as-deleted!) - (s/def ::delete-profile (s/keys :req-un [::profile-id])) (sv/defmethod ::delete-profile + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [::db/pool] :as cfg} {:keys [profile-id] :as params}] (db/with-atomic [conn pool] - (let [teams (get-owned-teams-with-participants conn profile-id) + (let [teams (#'profile/get-owned-teams-with-participants conn profile-id) deleted-at (dt/now)] ;; If we found owned teams with participants, we don't allow @@ -288,21 +191,3 @@ {:id profile-id}) (rph/with-transform {} (session/delete-fn cfg))))) - -(def sql:owned-teams - "with owner_teams as ( - select tpr.team_id as id - from team_profile_rel as tpr - where tpr.is_owner is true - and tpr.profile_id = ? - ) - select tpr.team_id as id, - count(tpr.profile_id) - 1 as participants - from team_profile_rel as tpr - where tpr.team_id in (select id from owner_teams) - and tpr.profile_id != ? - group by 1") - -(defn- get-owned-teams-with-participants - [conn profile-id] - (db/exec! conn [sql:owned-teams profile-id profile-id])) diff --git a/backend/src/app/rpc/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj index 6bfec336f..86b7ee015 100644 --- a/backend/src/app/rpc/queries/profile.clj +++ b/backend/src/app/rpc/queries/profile.clj @@ -6,81 +6,27 @@ (ns app.rpc.queries.profile (:require - [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] [app.rpc :as-alias rpc] + [app.rpc.commands.profile :as profile] + [app.rpc.doc :as-alias doc] [app.util.services :as sv] - [clojure.spec.alpha :as s] - [cuerdas.core :as str])) + [clojure.spec.alpha :as s])) -;; --- Helpers & Specs - -(s/def ::email ::us/email) -(s/def ::fullname ::us/string) -(s/def ::old-password ::us/string) -(s/def ::password ::us/string) -(s/def ::path ::us/string) -(s/def ::user ::us/uuid) -(s/def ::profile-id ::us/uuid) -(s/def ::theme ::us/string) - -;; --- Query: Profile (own) - -(declare decode-row) -(declare get-profile) -(declare strip-private-attrs) -(declare filter-props) - -(s/def ::profile - (s/keys :opt-un [::profile-id])) +(s/def ::profile ::profile/get-profile) (sv/defmethod ::profile - {::rpc/auth false} + {::rpc/auth false + ::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [::db/pool] :as cfg} {:keys [profile-id]}] ;; We need to return the anonymous profile object in two cases, when ;; no profile-id is in session, and when db call raises not found. In all other ;; cases we need to reraise the exception. (try - (-> (get-profile pool profile-id) - (strip-private-attrs) - (update :props filter-props)) + (-> (profile/get-profile pool profile-id) + (profile/strip-private-attrs) + (update :props profile/filter-props)) (catch Throwable _ {:id uuid/zero :fullname "Anonymous User"}))) - -(defn get-profile - "Get profile by id. Throws not-found exception if no profile found." - [conn id & {:as attrs}] - (-> (db/get-by-id conn :profile id attrs) - (decode-row))) - -(def ^:private sql:profile-by-email - "select p.* from profile as p - where p.email = ? - and (p.deleted_at is null or - p.deleted_at > now())") - -(defn get-profile-by-email - "Returns a profile looked up by email or `nil` if not match found." - [conn email] - (->> (db/exec! conn [sql:profile-by-email (str/lower email)]) - (map decode-row) - (first))) - -;; --- HELPERS - -(defn strip-private-attrs - "Only selects a publicly visible profile attrs." - [row] - (dissoc row :password :deleted-at)) - -(defn filter-props - "Removes all namespace qualified props from `props` attr." - [props] - (into {} (filter (fn [[k _]] (simple-ident? k))) props)) - -(defn decode-row - [{:keys [props] :as row}] - (cond-> row - (db/pgobject? props "jsonb") - (assoc :props (db/decode-transit-pgobject props)))) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 1561626d8..d9b03a099 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -12,8 +12,8 @@ [app.common.pprint :as p] [app.common.spec :as us] [app.db :as db] - [app.rpc.commands.auth :as cmd.auth] - [app.rpc.queries.profile :as profile] + [app.rpc.commands.auth :as auth] + [app.rpc.commands.profile :as profile] [app.srepl.fixes :as f] [app.srepl.helpers :as h] [app.util.blob :as blob] @@ -73,7 +73,7 @@ pool (:app.db/pool system) profile (profile/get-profile-by-email pool email)] - (cmd.auth/send-email-verification! pool sprops profile) + (auth/send-email-verification! pool sprops profile) :email-sent)) (defn mark-profile-as-active! diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 75f83b7c3..604a27943 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -6,14 +6,14 @@ (ns backend-tests.rpc-profile-test (:require - [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.rpc :as-alias rpc] [app.rpc.commands.auth :as cauth] - [app.rpc.mutations.profile :as profile] [app.tokens :as tokens] [app.util.time :as dt] + [backend-tests.helpers :as th] [clojure.java.io :as io] [clojure.test :as t] [cuerdas.core :as str] @@ -67,9 +67,9 @@ (t/deftest profile-query-and-manipulation (let [profile (th/create-profile* 1)] (t/testing "query profile" - (let [data {::th/type :profile - :profile-id (:id profile)} - out (th/query! data)] + (let [data {::th/type :get-profile + ::rpc/profile-id (:id profile)} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -82,20 +82,20 @@ (t/testing "update profile" (let [data (assoc profile ::th/type :update-profile - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :fullname "Full Name" :lang "en" :theme "dark") - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (map? (:result out))))) (t/testing "query profile after update" - (let [data {::th/type :profile - :profile-id (:id profile)} - out (th/query! data)] + (let [data {::th/type :get-profile + ::rpc/profile-id (:id profile)} + out (th/command! data)] #_(th/print-result! out) (t/is (nil? (:error out))) @@ -107,12 +107,12 @@ (t/testing "update photo" (let [data {::th/type :update-profile-photo - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :file {:filename "sample.jpg" :size 123123 :path (th/tempfile "backend_tests/test_files/sample.jpg") :mtype "image/jpeg"}} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))))) @@ -131,8 +131,8 @@ ;; Request profile to be deleted (let [params {::th/type :delete-profile - :profile-id (:id prof)} - out (th/mutation! params)] + ::rpc/profile-id (:id prof)} + out (th/command! params)] (t/is (nil? (:error out)))) ;; query files after profile soft deletion @@ -154,9 +154,9 @@ (t/is (dt/instant? (:deleted-at row)))) ;; query profile after delete - (let [params {::th/type :profile - :profile-id (:id prof)} - out (th/query! params)] + (let [params {::th/type :get-profile + ::rpc/profile-id (:id prof)} + out (th/command! params)] ;; (th/print-result! out) (let [result (:result out)] (t/is (= uuid/zero (:id result))))))) @@ -174,7 +174,7 @@ (let [data {::th/type :prepare-register-profile :email "user@example.com" :password "foobar"} - out (th/mutation! data) + out (th/command! data) token (get-in out [:result :token])] (t/is (string? token)) @@ -183,7 +183,7 @@ (let [data {::th/type :register-profile :fullname "foobar" :accept-terms-and-privacy true} - out (th/mutation! data)] + out (th/command! data)] (let [error (:error out)] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) @@ -195,7 +195,7 @@ :fullname "foobar" :accept-terms-and-privacy true :accept-newsletter-subscription true}] - (let [{:keys [result error]} (th/mutation! data)] + (let [{:keys [result error]} (th/command! data)] (t/is (nil? error)))) )) @@ -413,11 +413,11 @@ (let [profile (th/create-profile* 1) pool (:app.db/pool th/*system*) data {::th/type :request-email-change - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :email "user1@example.com"}] ;; without complaints - (let [out (th/mutation! data)] + (let [out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:result out))) (let [mock @mock] @@ -426,14 +426,14 @@ ;; with complaints (th/create-global-complaint-for pool {:type :complaint :email (:email data)}) - (let [out (th/mutation! data)] + (let [out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:result out))) (t/is (= 2 (:call-count @mock)))) ;; with bounces (th/create-global-complaint-for pool {:type :bounce :email (:email data)}) - (let [out (th/mutation! data) + (let [out (th/command! data) error (:error out)] ;; (th/print-result! out) (t/is (th/ex-info? error)) @@ -448,9 +448,9 @@ (let [profile (th/create-profile* 1) pool (:app.db/pool th/*system*) data {::th/type :request-email-change - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :email "user1@example.com"} - out (th/mutation! data)] + out (th/command! data)] ;; (th/print-result! out) (t/is (false? (:called? @mock))) @@ -467,7 +467,7 @@ ;; with invalid email (let [data (assoc data :email "foo@bar.com") - out (th/mutation! data)] + out (th/command! data)] (t/is (nil? (:result out))) (t/is (= 0 (:call-count @mock)))) @@ -512,10 +512,10 @@ (t/deftest update-profile-password (let [profile (th/create-profile* 1) data {::th/type :update-profile-password - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :old-password "123123" :password "foobarfoobar"} - out (th/mutation! data)] + out (th/command! data)] (t/is (nil? (:error out))) (t/is (nil? (:result out))) )) @@ -524,10 +524,10 @@ (t/deftest update-profile-password-bad-old-password (let [profile (th/create-profile* 1) data {::th/type :update-profile-password - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :old-password "badpassword" :password "foobarfoobar"} - {:keys [result error] :as out} (th/mutation! data)] + {:keys [result error] :as out} (th/command! data)] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) (t/is (th/ex-of-code? error :old-password-not-match)))) @@ -536,10 +536,10 @@ (t/deftest update-profile-password-email-as-password (let [profile (th/create-profile* 1) data {::th/type :update-profile-password - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :old-password "123123" :password "profile1.test@nodomain.com"} - {:keys [result error] :as out} (th/mutation! data)] + {:keys [result error] :as out} (th/command! data)] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) (t/is (th/ex-of-code? error :email-as-password)))) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 906ddfd7a..5cf2932ab 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -124,7 +124,7 @@ (ptk/reify ::fetch-profile ptk/WatchEvent (watch [_ _ _] - (->> (rp/query! :profile) + (->> (rp/cmd! :get-profile) (rx/map profile-fetched))))) ;; --- EVENT: INITIALIZE PROFILE @@ -207,7 +207,7 @@ ;; the returned profile is an NOT authenticated profile, we ;; proceed to logout and show an error message. - (->> (rp/command! :login-with-password (d/without-nils params)) + (->> (rp/cmd! :login-with-password (d/without-nils params)) (rx/merge-map (fn [data] (rx/merge (rx/of (fetch-profile)) @@ -293,7 +293,7 @@ (ptk/reify ::logout ptk/WatchEvent (watch [_ _ _] - (->> (rp/command! :logout) + (->> (rp/cmd! :logout) (rx/delay-at-least 300) (rx/catch (constantly (rx/of 1))) (rx/map #(logged-out params))))))) @@ -309,7 +309,7 @@ (let [mdata (meta data) on-success (:on-success mdata identity) on-error (:on-error mdata rx/throw)] - (->> (rp/mutation :update-profile (dissoc data :props)) + (->> (rp/cmd! :update-profile (dissoc data :props)) (rx/catch on-error) (rx/mapcat (fn [_] @@ -333,7 +333,7 @@ (let [{:keys [on-error on-success] :or {on-error identity on-success identity}} (meta data)] - (->> (rp/mutation :request-email-change data) + (->> (rp/cmd! :request-email-change data) (rx/tap on-success) (rx/catch on-error)))))) @@ -343,7 +343,7 @@ (ptk/reify ::cancel-email-change ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation :cancel-email-change {}) + (->> (rp/cmd! :cancel-email-change {}) (rx/map (constantly (fetch-profile))))))) ;; --- Update Password (Form) @@ -364,7 +364,7 @@ on-success identity}} (meta data) params {:old-password (:password-old data) :password (:password-1 data)}] - (->> (rp/mutation :update-profile-password params) + (->> (rp/cmd! :update-profile-password params) (rx/tap on-success) (rx/catch (fn [err] (on-error err) @@ -382,7 +382,7 @@ ;; the response value of update-profile-props RPC call ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation :update-profile-props {:props props}) + (->> (rp/cmd! :update-profile-props {:props props}) (rx/map (constantly (fetch-profile))))))) (defn mark-onboarding-as-viewed @@ -394,7 +394,7 @@ (let [version (or version (:main @cf/version)) props {:onboarding-viewed true :release-notes-viewed version}] - (->> (rp/mutation :update-profile-props {:props props}) + (->> (rp/cmd! :update-profile-props {:props props}) (rx/map (constantly (fetch-profile))))))))) (defn mark-questions-as-answered @@ -407,7 +407,7 @@ ptk/WatchEvent (watch [_ _ _] (let [props {:onboarding-questions-answered true}] - (->> (rp/mutation :update-profile-props {:props props}) + (->> (rp/cmd! :update-profile-props {:props props}) (rx/map (constantly (fetch-profile)))))))) @@ -431,7 +431,7 @@ (->> (rx/of file) (rx/map di/validate-file) (rx/map prepare) - (rx/mapcat #(rp/mutation :update-profile-photo %)) + (rx/mapcat #(rp/cmd! :update-profile-photo %)) (rx/do on-success) (rx/map (constantly (fetch-profile))) (rx/catch on-error)))))) @@ -460,7 +460,7 @@ ptk/WatchEvent (watch [_ state _] (let [share-id (-> state :viewer-local :share-id)] - (->> (rp/command! :get-profiles-for-file-comments {:team-id team-id :share-id share-id}) + (->> (rp/cmd! :get-profiles-for-file-comments {:team-id team-id :share-id share-id}) (rx/map #(partial fetched %)))))))) ;; --- EVENT: request-account-deletion @@ -473,7 +473,7 @@ (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta params)] - (->> (rp/mutation :delete-profile {}) + (->> (rp/cmd! :delete-profile {}) (rx/tap on-success) (rx/delay-at-least 300) (rx/catch (constantly (rx/of 1))) @@ -495,7 +495,7 @@ :or {on-error rx/throw on-success identity}} (meta data)] - (->> (rp/command! :request-profile-recovery data) + (->> (rp/cmd! :request-profile-recovery data) (rx/tap on-success) (rx/catch on-error)))))) @@ -514,7 +514,7 @@ (let [{:keys [on-error on-success] :or {on-error rx/throw on-success identity}} (meta data)] - (->> (rp/command! :recover-profile data) + (->> (rp/cmd! :recover-profile data) (rx/tap on-success) (rx/catch on-error)))))) @@ -525,7 +525,7 @@ (ptk/reify ::create-demo-profile ptk/WatchEvent (watch [_ _ _] - (->> (rp/command! :create-demo-profile {}) + (->> (rp/cmd! :create-demo-profile {}) (rx/map login))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index e2f79a913..61942fd5d 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -214,7 +214,7 @@ :features features}] (when (:id params) - (->> (rp/mutation :update-file params) + (->> (rp/cmd! :update-file params) (rx/ignore))))))) (defn update-persistence-status diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index b5b67fefc..be89b473e 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -23,6 +23,7 @@ (derive :get-team-stats ::query) (derive :get-team-invitations ::query) (derive :get-team-shared-files ::query) +(derive :get-profile ::query) (defn handle-response [{:keys [status body] :as response}] @@ -191,3 +192,12 @@ :body (http/form-data params)}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) + +(defmethod command ::multipart-upload + [id params] + (->> (http/send! {:method :post + :uri (u/join @cf/public-uri "api/rpc/command/" (name id)) + :credentials "include" + :body (http/form-data params)}) + (rx/map http/conditional-decode-transit) + (rx/mapcat handle-response))) diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index d6ffde3fe..c704cadfc 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -99,7 +99,7 @@ ;; 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) + (->> (rp/command! :get-profile) (rx/subs (fn [{:keys [id] :as profile}] (if (= id uuid/zero) (st/emit! (rt/nav :auth-login)) From ed87814f504990217fa036d7ec1e1d1253dd0a4d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 16 Jan 2023 14:08:58 +0100 Subject: [PATCH 05/16] :bug: Properly handle storage features on binfile import --- backend/src/app/rpc/commands/binfile.clj | 72 ++++++++++++++++-------- backend/src/app/rpc/commands/files.clj | 9 ++- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 22d994441..f521be1a9 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.exceptions :as ex] + [app.common.files.features :as ffeat] [app.common.logging :as l] [app.common.pages.migrations :as pmg] [app.common.spec :as us] @@ -28,6 +29,7 @@ [app.tasks.file-gc] [app.util.blob :as blob] [app.util.fressian :as fres] + [app.util.objects-map :as omap] [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] @@ -607,12 +609,23 @@ (vswap! *state* update :index update-index files) (vswap! *state* assoc :version version :files files))) +(defn- postprocess-file + [data] + (let [omap-wrap ffeat/*wrap-with-objects-map-fn* + pmap-wrap ffeat/*wrap-with-pointer-map-fn*] + (-> data + (update :pages-index update-vals #(update % :objects omap-wrap)) + (update :pages-index update-vals pmap-wrap) + (update :components update-vals #(update % :objects omap-wrap)) + (update :components pmap-wrap)))) + (defmethod read-section :v1/files [{:keys [conn ::input ::migrate? ::project-id ::timestamp ::overwrite?]}] (doseq [expected-file-id (-> *state* deref :files)] - (let [file (read-obj! input) - media' (read-obj! input) - file-id (:id file)] + (let [file (read-obj! input) + media' (read-obj! input) + file-id (:id file) + features files/default-features] (when (not= file-id expected-file-id) (ex/raise :type :validation @@ -627,33 +640,42 @@ (l/debug :hint "update media references" ::l/async false) (vswap! *state* update :media into (map #(update % :id lookup-index)) media') - (l/debug :hint "processing file" :file-id file-id ::l/async false) + (l/debug :hint "processing file" :file-id file-id ::features features ::l/async false) - (let [file-id' (lookup-index file-id) - data (-> (:data file) - (assoc :id file-id') - (cond-> migrate? (pmg/migrate-data)) - (update :pages-index relink-shapes) - (update :components relink-shapes) - (update :media relink-media)) + (binding [ffeat/*current* features + ffeat/*wrap-with-objects-map-fn* (if (features "storage/objects-map") omap/wrap identity) + ffeat/*wrap-with-pointer-map-fn* (if (features "storage/pointer-map") pmap/wrap identity) + pmap/*tracked* (atom {})] - params {:id file-id' - :project-id project-id - :name (:name file) - :revn (:revn file) - :is-shared (:is-shared file) - :data (blob/encode data) - :created-at timestamp - :modified-at timestamp}] + (let [file-id' (lookup-index file-id) + data (-> (:data file) + (assoc :id file-id') + (cond-> migrate? (pmg/migrate-data)) + (update :pages-index relink-shapes) + (update :components relink-shapes) + (update :media relink-media) + (postprocess-file)) - (l/debug :hint "create file" :id file-id' ::l/async false) + params {:id file-id' + :project-id project-id + :features (db/create-array conn "text" features) + :name (:name file) + :revn (:revn file) + :is-shared (:is-shared file) + :data (blob/encode data) + :created-at timestamp + :modified-at timestamp}] - (if overwrite? - (create-or-update-file conn params) - (db/insert! conn :file params)) + (l/debug :hint "create file" :id file-id' ::l/async false) - (when overwrite? - (db/delete! conn :file-thumbnail {:file-id file-id'})))))) + (if overwrite? + (create-or-update-file conn params) + (db/insert! conn :file params)) + + (files/persist-pointers! conn file-id') + + (when overwrite? + (db/delete! conn :file-thumbnail {:file-id file-id'}))))))) (defmethod read-section :v1/rels [{:keys [conn ::input ::timestamp]}] diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index b05e50bd9..25688cd08 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -15,6 +15,7 @@ [app.common.spec :as us] [app.common.types.file :as ctf] [app.common.types.shape-tree :as ctt] + [app.config :as cf] [app.db :as db] [app.db.sql :as sql] [app.loggers.audit :as-alias audit] @@ -43,7 +44,13 @@ "storage/pointer-map" "components/v2"}) -(def default-features #{}) +(def default-features + (cond-> #{} + (contains? cf/flags :feature-storage-pointer-map) + (conj "storage/pointer-map") + + (contains? cf/flags :feature-storage-objects-map) + (conj "storage/objects-map"))) ;; --- SPECS From dc77c6b6551d33e12f6d855d03f1fab84d75afb9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 16 Jan 2023 16:50:42 +0100 Subject: [PATCH 06/16] :sparkles: Remove deprecated code and reoganize file related methods --- backend/src/app/http/debug.clj | 2 +- backend/src/app/rpc.clj | 8 +- .../{files/create.clj => files_create.clj} | 2 +- .../{files/temp.clj => files_temp.clj} | 8 +- .../{files/update.clj => files_update.clj} | 2 +- backend/src/app/rpc/mutations/files.clj | 237 ------------------ backend/src/app/rpc/queries/files.clj | 183 -------------- backend/test/backend_tests/helpers.clj | 4 +- .../test/backend_tests/rpc_profile_test.clj | 8 +- .../test/backend_tests/rpc_project_test.clj | 17 +- 10 files changed, 25 insertions(+), 446 deletions(-) rename backend/src/app/rpc/commands/{files/create.clj => files_create.clj} (99%) rename backend/src/app/rpc/commands/{files/temp.clj => files_temp.clj} (92%) rename backend/src/app/rpc/commands/{files/update.clj => files_update.clj} (99%) delete mode 100644 backend/src/app/rpc/mutations/files.clj delete mode 100644 backend/src/app/rpc/queries/files.clj diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index d2cbb6f7a..1d342b9fb 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -16,7 +16,7 @@ [app.http.middleware :as mw] [app.http.session :as session] [app.rpc.commands.binfile :as binf] - [app.rpc.commands.files.create :refer [create-file]] + [app.rpc.commands.files-create :refer [create-file]] [app.rpc.commands.profile :as profile] [app.util.blob :as blob] [app.util.template :as tmpl] diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 24fb1637d..2e06aa942 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -300,7 +300,6 @@ (let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)] (->> (sv/scan-ns 'app.rpc.queries.projects - 'app.rpc.queries.files 'app.rpc.queries.teams 'app.rpc.queries.profile 'app.rpc.queries.viewer @@ -314,7 +313,6 @@ (->> (sv/scan-ns 'app.rpc.mutations.media 'app.rpc.mutations.profile - 'app.rpc.mutations.files 'app.rpc.mutations.projects 'app.rpc.mutations.teams 'app.rpc.mutations.fonts @@ -334,9 +332,9 @@ 'app.rpc.commands.comments 'app.rpc.commands.demo 'app.rpc.commands.files - 'app.rpc.commands.files.create - 'app.rpc.commands.files.temp - 'app.rpc.commands.files.update + 'app.rpc.commands.files-create + 'app.rpc.commands.files-temp + 'app.rpc.commands.files-update 'app.rpc.commands.ldap 'app.rpc.commands.management 'app.rpc.commands.media diff --git a/backend/src/app/rpc/commands/files/create.clj b/backend/src/app/rpc/commands/files_create.clj similarity index 99% rename from backend/src/app/rpc/commands/files/create.clj rename to backend/src/app/rpc/commands/files_create.clj index 2d4a7a808..19bef5768 100644 --- a/backend/src/app/rpc/commands/files/create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.rpc.commands.files.create +(ns app.rpc.commands.files-create (:require [app.common.data :as d] [app.common.files.features :as ffeat] diff --git a/backend/src/app/rpc/commands/files/temp.clj b/backend/src/app/rpc/commands/files_temp.clj similarity index 92% rename from backend/src/app/rpc/commands/files/temp.clj rename to backend/src/app/rpc/commands/files_temp.clj index 193bb285a..78fabfd04 100644 --- a/backend/src/app/rpc/commands/files/temp.clj +++ b/backend/src/app/rpc/commands/files_temp.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.rpc.commands.files.temp +(ns app.rpc.commands.files-temp (:require [app.common.exceptions :as ex] [app.common.pages :as cp] @@ -13,8 +13,8 @@ [app.db :as db] [app.rpc :as-alias rpc] [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.files-create :refer [create-file]] + [app.rpc.commands.files-update :as-alias files.update] [app.rpc.doc :as-alias doc] [app.rpc.queries.projects :as proj] [app.util.blob :as blob] @@ -40,7 +40,7 @@ [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] (db/with-atomic [conn pool] (proj/check-edition-permissions! conn profile-id project-id) - (files.create/create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1}))))) + (create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1}))))) ;; --- MUTATION COMMAND: update-temp-file diff --git a/backend/src/app/rpc/commands/files/update.clj b/backend/src/app/rpc/commands/files_update.clj similarity index 99% rename from backend/src/app/rpc/commands/files/update.clj rename to backend/src/app/rpc/commands/files_update.clj index 14330e4c1..89c39ff04 100644 --- a/backend/src/app/rpc/commands/files/update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.rpc.commands.files.update +(ns app.rpc.commands.files-update (:require [app.common.exceptions :as ex] [app.common.files.features :as ffeat] diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj deleted file mode 100644 index 273b9313b..000000000 --- a/backend/src/app/rpc/mutations/files.clj +++ /dev/null @@ -1,237 +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.files - (:require - [app.common.exceptions :as ex] - [app.common.logging :as l] - [app.common.spec :as us] - [app.db :as db] - [app.loggers.audit :as audit] - [app.rpc.climit :as-alias climit] - [app.rpc.commands.files :as cmd.files] - [app.rpc.commands.files.create :as cmd.files.create] - [app.rpc.commands.files.temp :as cmd.files.temp] - [app.rpc.commands.files.update :as cmd.files.update] - [app.rpc.doc :as-alias doc] - [app.rpc.helpers :as rph] - [app.rpc.queries.projects :as proj] - [app.util.services :as sv] - [app.util.time :as dt] - [clojure.spec.alpha :as s])) - -;; --- Mutation: Create File - -(s/def ::create-file ::cmd.files.create/create-file) - -(sv/defmethod ::create-file - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id project-id features components-v2] :as params}] - (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id project-id) - (let [team-id (cmd.files/get-team-id conn project-id) - features (cond-> (or features #{}) - ;; BACKWARD COMPATIBILITY with the components-v2 param - components-v2 (conj "components/v2")) - params (assoc params :features features)] - (-> (cmd.files.create/create-file conn params) - (vary-meta assoc ::audit/props {:team-id team-id}))))) - - -;; --- Mutation: Rename File - -(s/def ::rename-file ::cmd.files/rename-file) - -(sv/defmethod ::rename-file - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id id) - (cmd.files/rename-file conn params))) - - -;; --- Mutation: Set File shared - -(s/def ::set-file-shared ::cmd.files/set-file-shared) - -(sv/defmethod ::set-file-shared - {::doc/added "1.2" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id is-shared] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id id) - (when-not is-shared - (cmd.files/absorb-library conn params) - (cmd.files/unlink-files conn params)) - (cmd.files/set-file-shared conn params))) - -;; --- Mutation: Delete File - -(s/def ::delete-file ::cmd.files/delete-file) - -(sv/defmethod ::delete-file - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id id) - (cmd.files/absorb-library conn params) - (cmd.files/mark-file-deleted conn params) - nil)) - -;; --- Mutation: Link file to library - -(s/def ::link-file-to-library ::cmd.files/link-file-to-library) - -(sv/defmethod ::link-file-to-library - {::doc/added "1.3" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}] - (when (= file-id library-id) - (ex/raise :type :validation - :code :invalid-library - :hint "A file cannot be linked to itself")) - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/check-edition-permissions! conn profile-id library-id) - (cmd.files/link-file-to-library conn params))) - -;; --- Mutation: Unlink file from library - -(s/def ::unlink-file-from-library ::cmd.files/unlink-file-from-library) - -(sv/defmethod ::unlink-file-from-library - {::doc/added "1.3" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/unlink-file-from-library conn params))) - - -;; --- Mutation: Update synchronization status of a link - -(s/def ::update-sync ::cmd.files/update-file-library-sync-status) - -(sv/defmethod ::update-sync - {::doc/added "1.10" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/update-sync conn params))) - - -;; --- Mutation: Ignore updates in linked files - -(declare ignore-sync) - -(s/def ::ignore-sync ::cmd.files/ignore-file-library-sync-status) - -(sv/defmethod ::ignore-sync - {::doc/added "1.10" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/ignore-sync conn params))) - - -;; --- MUTATION: update-file - -(s/def ::components-v2 ::us/boolean) -(s/def ::update-file - (s/and ::cmd.files.update/update-file - (s/keys :opt-un [::components-v2]))) - -(sv/defmethod ::update-file - {::climit/queue :update-file - ::climit/key-fn :id - ::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id features components-v2] :as params}] - (db/with-atomic [conn pool] - (db/xact-lock! conn id) - (cmd.files/check-edition-permissions! conn profile-id id) - - (let [;; BACKWARD COMPATIBILITY with the components-v2 parameter - features (cond-> (or features #{}) - components-v2 (conj "components/v2")) - tpoint (dt/tpoint) - params (assoc params :features features) - cfg (assoc cfg :conn conn)] - - (-> (cmd.files.update/update-file cfg params) - (rph/with-defer #(let [elapsed (tpoint)] - (l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))) - -;; --- Mutation: upsert object thumbnail - -(s/def ::upsert-file-object-thumbnail ::cmd.files/upsert-file-object-thumbnail) - -(sv/defmethod ::upsert-file-object-thumbnail - {::doc/added "1.13" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/upsert-file-object-thumbnail! conn params) - nil)) - - -;; --- Mutation: upsert file thumbnail - -(s/def ::upsert-file-thumbnail ::cmd.files/upsert-file-thumbnail) - -(sv/defmethod ::upsert-file-thumbnail - "Creates or updates the file thumbnail. Mainly used for paint the - grid thumbnails." - {::doc/added "1.13" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id file-id) - (cmd.files/upsert-file-thumbnail conn params) - nil)) - - -;; --- MUTATION COMMAND: create-temp-file - -(s/def ::create-temp-file ::cmd.files.temp/create-temp-file) - -(sv/defmethod ::create-temp-file - {::doc/added "1.7" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] - (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id project-id) - (cmd.files.create/create-file conn (assoc params :deleted-at (dt/in-future {:days 1}))))) - -;; --- MUTATION COMMAND: update-temp-file - -(s/def ::update-temp-file ::cmd.files.temp/update-temp-file) - -(sv/defmethod ::update-temp-file - {::doc/added "1.7" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.files.temp/update-temp-file conn params) - nil)) - -;; --- MUTATION COMMAND: persist-temp-file - -(s/def ::persist-temp-file ::cmd.files.temp/persist-temp-file) - -(sv/defmethod ::persist-temp-file - {::doc/added "1.7" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] - (db/with-atomic [conn pool] - (cmd.files/check-edition-permissions! conn profile-id id) - (cmd.files.temp/persist-temp-file conn params))) diff --git a/backend/src/app/rpc/queries/files.clj b/backend/src/app/rpc/queries/files.clj deleted file mode 100644 index 3dc5d9f92..000000000 --- a/backend/src/app/rpc/queries/files.clj +++ /dev/null @@ -1,183 +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.files - (:require - [app.common.spec :as us] - [app.db :as db] - [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.util.services :as sv] - [clojure.spec.alpha :as s])) - -;; --- Query: Project Files - -(s/def ::project-files ::files/get-project-files) - -(sv/defmethod ::project-files - {::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) - (files/get-project-files conn project-id))) - -;; --- Query: File (By ID) - -(s/def ::components-v2 ::us/boolean) -(s/def ::file - (s/and ::files/get-file - (s/keys :opt-un [::components-v2]))) - -(defn get-file - [conn id features] - (let [file (files/get-file conn id features) - thumbs (files/get-object-thumbnails conn id)] - (assoc file :thumbnails thumbs))) - -(sv/defmethod ::file - "Retrieve a file by its ID. Only authenticated users." - {::doc/added "1.0" - ::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 (files/get-permissions pool profile-id id) - ;; BACKWARD COMPATIBILTY with the components-v2 parameter - features (cond-> (or features #{}) - components-v2 (conj "components/v2"))] - - (files/check-read-permissions! perms) - (-> (get-file conn id features) - (assoc :permissions perms))))) - -;; --- QUERY: page - -(s/def ::page - (s/and ::files/get-page - (s/keys :opt-un [::components-v2]))) - -(sv/defmethod ::page - "Retrieves the page data from file and returns it. If no page-id is - specified, the first page will be returned. If object-id is - specified, only that object and its children will be returned in the - page objects data structure. - - If you specify the object-id, the page-id parameter becomes - mandatory. - - Mainly used for rendering purposes." - {::doc/added "1.5" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as params}] - (with-open [conn (db/open pool)] - (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)] - - (files/get-page conn params)))) - -;; --- QUERY: file-data-for-thumbnail - -(s/def ::file-data-for-thumbnail - (s/and ::files/get-file-data-for-thumbnail - (s/keys :opt-un [::components-v2]))) - -(sv/defmethod ::file-data-for-thumbnail - "Retrieves the data for generate the thumbnail of the file. Used - mainly for render thumbnails on dashboard." - {::doc/added "1.11" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as props}] - (with-open [conn (db/open pool)] - (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 (files/get-file conn file-id features)] - {:file-id file-id - :revn (:revn file) - :page (files/get-file-data-for-thumbnail conn file)}))) - -;; --- Query: Shared Library Files - -(s/def ::team-shared-files ::files/get-team-shared-files) - -(sv/defmethod ::team-shared-files - {::doc/added "1.3" - ::doc/deprecated "1.17"} - [{: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) - (files/get-team-shared-files conn params))) - - -;; --- Query: File Libraries used by a File - -(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)] - (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 ::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)] - (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 ::files/get-team-recent-files) - -(sv/defmethod ::team-recent-files - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] - (with-open [conn (db/open pool)] - (teams/check-read-permissions! conn profile-id team-id) - (files/get-team-recent-files conn team-id))) - - -;; --- QUERY: 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)] - (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 ::search/search-files) - -(sv/defmethod ::search-files - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{: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/helpers.clj b/backend/test/backend_tests/helpers.clj index 4656f5f17..c656d6c74 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -23,8 +23,8 @@ [app.rpc :as-alias rpc] [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.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] diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 604a27943..df180e05f 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -136,10 +136,10 @@ (t/is (nil? (:error out)))) ;; query files after profile soft deletion - (let [params {::th/type :project-files - :project-id (:default-project-id prof) - :profile-id (:id prof)} - out (th/query! params)] + (let [params {::th/type :get-project-files + ::rpc/profile-id (:id prof) + :project-id (:default-project-id prof)} + out (th/command! params)] ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (= 1 (count (:result out))))) diff --git a/backend/test/backend_tests/rpc_project_test.clj b/backend/test/backend_tests/rpc_project_test.clj index 24c745a83..ade2cf953 100644 --- a/backend/test/backend_tests/rpc_project_test.clj +++ b/backend/test/backend_tests/rpc_project_test.clj @@ -9,6 +9,7 @@ [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.db :as db] + [app.rpc :as-alias rpc] [app.http :as http] [app.util.time :as dt] [clojure.test :as t])) @@ -214,10 +215,10 @@ (t/is (= 0 (:processed result)))) ;; query the list of files of a after soft deletion - (let [data {::th/type :project-files - :project-id (:id project) - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-project-files + ::rpc/profile-id (:id profile1) + :project-id (:id project)} + out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) (let [result (:result out)] @@ -228,10 +229,10 @@ (t/is (= 1 (:processed result)))) ;; query the list of files of a after hard deletion - (let [data {::th/type :project-files - :project-id (:id project) - :profile-id (:id profile1)} - out (th/query! data)] + (let [data {::th/type :get-project-files + ::rpc/profile-id (:id profile1) + :project-id (:id project)} + out (th/command! data)] ;; (th/print-result! out) (let [error (:error out) error-data (ex-data error)] From dba7a9d424bfb7c691767f83217809e95b510087 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 16 Jan 2023 17:13:17 +0100 Subject: [PATCH 07/16] :sparkles: Move projects queries and mutations to commands --- backend/src/app/rpc.clj | 4 +- backend/src/app/rpc/commands/binfile.clj | 2 +- backend/src/app/rpc/commands/files.clj | 2 +- backend/src/app/rpc/commands/files_create.clj | 4 +- backend/src/app/rpc/commands/files_temp.clj | 4 +- backend/src/app/rpc/commands/management.clj | 2 +- backend/src/app/rpc/commands/projects.clj | 268 ++++++++++++++++++ backend/src/app/rpc/mutations/projects.clj | 12 +- backend/src/app/rpc/queries/fonts.clj | 2 +- backend/src/app/rpc/queries/projects.clj | 116 +------- backend/test/backend_tests/rpc_font_test.clj | 14 +- frontend/src/app/main/data/dashboard.cljs | 12 +- frontend/src/app/main/data/workspace.cljs | 4 +- .../src/app/main/ui/dashboard/file_menu.cljs | 2 +- 14 files changed, 311 insertions(+), 137 deletions(-) create mode 100644 backend/src/app/rpc/commands/projects.clj diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 2e06aa942..53dda5bdb 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -339,6 +339,7 @@ 'app.rpc.commands.management 'app.rpc.commands.media 'app.rpc.commands.profile + 'app.rpc.commands.projects 'app.rpc.commands.search 'app.rpc.commands.teams 'app.rpc.commands.verify-token @@ -356,8 +357,7 @@ ::sto/storage ::mtx/metrics ::main/props - ::wrk/executor - ] + ::wrk/executor] :opt [::climit ::rlimit] :req-un [::db/pool])) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index f521be1a9..10385915e 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -21,9 +21,9 @@ [app.media :as media] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as projects] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.projects :as projects] [app.storage :as sto] [app.storage.tmp :as tmp] [app.tasks.file-gc] diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 25688cd08..a7067ee1c 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -22,12 +22,12 @@ [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] [app.rpc.commands.files.thumbnails :as-alias thumbs] + [app.rpc.commands.projects :as projects] [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.util.blob :as blob] [app.util.pointer-map :as pmap] diff --git a/backend/src/app/rpc/commands/files_create.clj b/backend/src/app/rpc/commands/files_create.clj index 19bef5768..c6e904672 100644 --- a/backend/src/app/rpc/commands/files_create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -15,9 +15,9 @@ [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as projects] [app.rpc.doc :as-alias doc] [app.rpc.permissions :as perms] - [app.rpc.queries.projects :as proj] [app.rpc.quotes :as quotes] [app.util.blob :as blob] [app.util.objects-map :as omap] @@ -82,7 +82,7 @@ ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id project-id) + (projects/check-edition-permissions! conn profile-id project-id) (let [team-id (files/get-team-id conn project-id) params (assoc params :profile-id profile-id)] diff --git a/backend/src/app/rpc/commands/files_temp.clj b/backend/src/app/rpc/commands/files_temp.clj index 78fabfd04..ed3405d17 100644 --- a/backend/src/app/rpc/commands/files_temp.clj +++ b/backend/src/app/rpc/commands/files_temp.clj @@ -15,8 +15,8 @@ [app.rpc.commands.files :as files] [app.rpc.commands.files-create :refer [create-file]] [app.rpc.commands.files-update :as-alias files.update] + [app.rpc.commands.projects :as projects] [app.rpc.doc :as-alias doc] - [app.rpc.queries.projects :as proj] [app.util.blob :as blob] [app.util.services :as sv] [app.util.time :as dt] @@ -39,7 +39,7 @@ {::doc/added "1.17"} [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id project-id) + (projects/check-edition-permissions! conn profile-id project-id) (create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1}))))) ;; --- MUTATION COMMAND: update-temp-file diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 2337ff844..e0ed3d2d6 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -17,9 +17,9 @@ [app.rpc :as-alias rpc] [app.rpc.commands.binfile :as binfile] [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as proj] [app.rpc.commands.teams :as teams :refer [create-project-role create-project]] [app.rpc.doc :as-alias doc] - [app.rpc.queries.projects :as proj] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.services :as sv] diff --git a/backend/src/app/rpc/commands/projects.clj b/backend/src/app/rpc/commands/projects.clj new file mode 100644 index 000000000..518aec84a --- /dev/null +++ b/backend/src/app/rpc/commands/projects.clj @@ -0,0 +1,268 @@ +;; 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.projects + (:require + [app.common.spec :as us] + [app.db :as db] + [app.loggers.audit :as-alias audit] + [app.loggers.webhooks :as webhooks] + [app.rpc :as-alias rpc] + [app.rpc.commands.teams :as teams] + [app.rpc.doc :as-alias doc] + [app.rpc.helpers :as rph] + [app.rpc.permissions :as perms] + [app.rpc.quotes :as quotes] + [app.util.services :as sv] + [app.util.time :as dt] + [clojure.spec.alpha :as s])) + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) + +;; --- Check Project Permissions + +(def ^:private sql:project-permissions + "select tpr.is_owner, + tpr.is_admin, + tpr.can_edit + from team_profile_rel as tpr + inner join project as p on (p.team_id = tpr.team_id) + where p.id = ? + and tpr.profile_id = ? + union all + select ppr.is_owner, + ppr.is_admin, + ppr.can_edit + from project_profile_rel as ppr + where ppr.project_id = ? + and ppr.profile_id = ?") + +(defn- get-permissions + [conn profile-id project-id] + (let [rows (db/exec! conn [sql:project-permissions + project-id profile-id + project-id profile-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: Get projects + +(declare get-projects) + +(s/def ::team-id ::us/uuid) +(s/def ::get-projects + (s/keys :req [::rpc/profile-id] + :req-un [::team-id])) + +(sv/defmethod ::get-projects + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}] + (with-open [conn (db/open pool)] + (teams/check-read-permissions! conn profile-id team-id) + (get-projects conn profile-id team-id))) + +(def sql:projects + "select p.*, + coalesce(tpp.is_pinned, false) as is_pinned, + (select count(*) from file as f + where f.project_id = p.id + and deleted_at is null) as count + from project as p + inner join team as t on (t.id = p.team_id) + left join team_project_profile_rel as tpp + on (tpp.project_id = p.id and + tpp.team_id = p.team_id and + tpp.profile_id = ?) + where p.team_id = ? + and p.deleted_at is null + and t.deleted_at is null + order by p.modified_at desc") + +(defn get-projects + [conn profile-id team-id] + (db/exec! conn [sql:projects profile-id team-id])) + +;; --- QUERY: Get all projects + +(declare get-all-projects) + +(s/def ::get-all-projects + (s/keys :req [::rpc/profile-id])) + +(sv/defmethod ::get-all-projects + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id]}] + (with-open [conn (db/open pool)] + (get-all-projects conn profile-id))) + +(def sql:all-projects + "select p1.*, t.name as team_name, t.is_default as is_default_team + from project as p1 + inner join team as t on (t.id = p1.team_id) + where t.id in (select team_id + from team_profile_rel as tpr + where tpr.profile_id = ? + and (tpr.can_edit = true or + tpr.is_owner = true or + tpr.is_admin = true)) + and t.deleted_at is null + and p1.deleted_at is null + union + select p2.*, t.name as team_name, t.is_default as is_default_team + from project as p2 + inner join team as t on (t.id = p2.team_id) + where p2.id in (select project_id + from project_profile_rel as ppr + where ppr.profile_id = ? + and (ppr.can_edit = true or + ppr.is_owner = true or + ppr.is_admin = true)) + and t.deleted_at is null + and p2.deleted_at is null + order by team_name, name;") + +(defn get-all-projects + [conn profile-id] + (db/exec! conn [sql:all-projects profile-id profile-id])) + + +;; --- QUERY: Get project + +(s/def ::get-project + (s/keys :req [::rpc/profile-id] + :req-un [::id])) + +(sv/defmethod ::get-project + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id id]}] + (with-open [conn (db/open pool)] + (let [project (db/get-by-id conn :project id)] + (check-read-permissions! conn profile-id id) + project))) + + + +;; --- MUTATION: Create Project + +(s/def ::create-project + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::name] + :opt-un [::id])) + +(sv/defmethod ::create-project + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + (quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team + ::quotes/profile-id profile-id + ::quotes/team-id team-id}) + + (let [params (assoc params :profile-id profile-id) + project (teams/create-project conn params)] + (teams/create-project-role conn profile-id (:id project) :owner) + (db/insert! conn :team-project-profile-rel + {:project-id (:id project) + :profile-id profile-id + :team-id team-id + :is-pinned true}) + (assoc project :is-pinned true)))) + + +;; --- MUTATION: Toggle Project Pin + +(def ^:private + sql:update-project-pin + "insert into team_project_profile_rel (team_id, project_id, profile_id, is_pinned) + values (?, ?, ?, ?) + on conflict (team_id, project_id, profile_id) + do update set is_pinned=?") + +(s/def ::is-pinned ::us/boolean) +(s/def ::project-id ::us/uuid) +(s/def ::update-project-pin + (s/keys :req [::rpc/profile-id] + :req-un [::id ::team-id ::is-pinned])) + +(sv/defmethod ::update-project-pin + {::doc/added "1.18" + ::webhooks/batch-timeout (dt/duration "5s") + ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id) + ::webhooks/event? true} + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}] + (db/with-atomic [conn pool] + (check-edition-permissions! conn profile-id id) + (db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned]) + nil)) + +;; --- MUTATION: Rename Project + +(declare rename-project) + +(s/def ::rename-project + (s/keys :req [::rpc/profile-id] + :req-un [::name ::id])) + +(sv/defmethod ::rename-project + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}] + (db/with-atomic [conn pool] + (check-edition-permissions! conn profile-id id) + (let [project (db/get-by-id conn :project id ::db/for-update? true)] + (db/update! conn :project + {:name name} + {:id id}) + (rph/with-meta (rph/wrap) + {::audit/props {:team-id (:team-id project) + :prev-name (:name project)}})))) + +;; --- MUTATION: Delete Project + +(s/def ::delete-project + (s/keys :req [::rpc/profile-id] + :req-un [::id])) + +;; TODO: right now, we just don't allow delete default projects, in a +;; future we need to ensure raise a correct exception signaling that +;; this is not allowed. + +(sv/defmethod ::delete-project + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + (db/with-atomic [conn pool] + (check-edition-permissions! conn profile-id id) + (let [project (db/update! conn :project + {:deleted-at (dt/now)} + {:id id :is-default false})] + (rph/with-meta (rph/wrap) + {::audit/props {:team-id (:team-id project) + :name (:name project) + :created-at (:created-at project) + :modified-at (:modified-at project)}})))) + + diff --git a/backend/src/app/rpc/mutations/projects.clj b/backend/src/app/rpc/mutations/projects.clj index 1a49d4fc1..a400b9b6d 100644 --- a/backend/src/app/rpc/mutations/projects.clj +++ b/backend/src/app/rpc/mutations/projects.clj @@ -10,10 +10,10 @@ [app.db :as db] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] + [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.rpc.queries.projects :as proj] [app.rpc.quotes :as quotes] [app.util.services :as sv] [app.util.time :as dt] @@ -34,6 +34,7 @@ (sv/defmethod ::create-project {::doc/added "1.0" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] (db/with-atomic [conn pool] @@ -70,12 +71,13 @@ (sv/defmethod ::update-project-pin {::doc/added "1.0" + ::doc/deprecated "1.18" ::webhooks/batch-timeout (dt/duration "5s") ::webhooks/batch-key :id ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id profile-id team-id is-pinned] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id id) + (projects/check-edition-permissions! conn profile-id id) (db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned]) nil)) @@ -88,10 +90,11 @@ (sv/defmethod ::rename-project {::doc/added "1.0" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id profile-id name] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id id) + (projects/check-edition-permissions! conn profile-id id) (let [project (db/get-by-id conn :project id)] (db/update! conn :project {:name name} @@ -112,10 +115,11 @@ (sv/defmethod ::delete-project {::doc/added "1.0" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] (db/with-atomic [conn pool] - (proj/check-edition-permissions! conn profile-id id) + (projects/check-edition-permissions! conn profile-id id) (let [project (db/update! conn :project {:deleted-at (dt/now)} {:id id :is-default false})] diff --git a/backend/src/app/rpc/queries/fonts.clj b/backend/src/app/rpc/queries/fonts.clj index 6c9623c54..b6c96c59a 100644 --- a/backend/src/app/rpc/queries/fonts.clj +++ b/backend/src/app/rpc/queries/fonts.clj @@ -9,9 +9,9 @@ [app.common.spec :as us] [app.db :as db] [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as projects] [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])) diff --git a/backend/src/app/rpc/queries/projects.clj b/backend/src/app/rpc/queries/projects.clj index 64c4a9b42..4329c13cb 100644 --- a/backend/src/app/rpc/queries/projects.clj +++ b/backend/src/app/rpc/queries/projects.clj @@ -8,135 +8,39 @@ (:require [app.common.spec :as us] [app.db :as db] + [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] - [app.rpc.permissions :as perms] + [app.rpc.doc :as-alias doc] [app.util.services :as sv] [clojure.spec.alpha :as s])) -;; --- Check Project Permissions - -(def ^:private sql:project-permissions - "select tpr.is_owner, - tpr.is_admin, - tpr.can_edit - from team_profile_rel as tpr - inner join project as p on (p.team_id = tpr.team_id) - where p.id = ? - and tpr.profile_id = ? - union all - select ppr.is_owner, - ppr.is_admin, - ppr.can_edit - from project_profile_rel as ppr - where ppr.project_id = ? - and ppr.profile_id = ?") - -(defn- get-permissions - [conn profile-id project-id] - (let [rows (db/exec! conn [sql:project-permissions - project-id profile-id - project-id profile-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: Projects -(declare retrieve-projects) - (s/def ::team-id ::us/uuid) (s/def ::profile-id ::us/uuid) (s/def ::projects (s/keys :req-un [::profile-id ::team-id])) (sv/defmethod ::projects + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [pool]} {:keys [profile-id team-id]}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) - (retrieve-projects conn profile-id team-id))) - -(def sql:projects - "select p.*, - coalesce(tpp.is_pinned, false) as is_pinned, - (select count(*) from file as f - where f.project_id = p.id - and deleted_at is null) as count - from project as p - inner join team as t on (t.id = p.team_id) - left join team_project_profile_rel as tpp - on (tpp.project_id = p.id and - tpp.team_id = p.team_id and - tpp.profile_id = ?) - where p.team_id = ? - and p.deleted_at is null - and t.deleted_at is null - order by p.modified_at desc") - -(defn retrieve-projects - [conn profile-id team-id] - (db/exec! conn [sql:projects profile-id team-id])) - + (projects/get-projects conn profile-id team-id))) ;; --- Query: All projects -(declare retrieve-all-projects) - (s/def ::profile-id ::us/uuid) (s/def ::all-projects (s/keys :req-un [::profile-id])) (sv/defmethod ::all-projects + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [pool]} {:keys [profile-id]}] (with-open [conn (db/open pool)] - (retrieve-all-projects conn profile-id))) - -(def sql:all-projects - "select p1.*, t.name as team_name, t.is_default as is_default_team - from project as p1 - inner join team as t on (t.id = p1.team_id) - where t.id in (select team_id - from team_profile_rel as tpr - where tpr.profile_id = ? - and (tpr.can_edit = true or - tpr.is_owner = true or - tpr.is_admin = true)) - and t.deleted_at is null - and p1.deleted_at is null - union - select p2.*, t.name as team_name, t.is_default as is_default_team - from project as p2 - inner join team as t on (t.id = p2.team_id) - where p2.id in (select project_id - from project_profile_rel as ppr - where ppr.profile_id = ? - and (ppr.can_edit = true or - ppr.is_owner = true or - ppr.is_admin = true)) - and t.deleted_at is null - and p2.deleted_at is null - order by team_name, name;") - -(defn retrieve-all-projects - [conn profile-id] - (db/exec! conn [sql:all-projects profile-id profile-id])) - + (projects/get-all-projects conn profile-id))) ;; --- Query: Project @@ -145,9 +49,11 @@ (s/keys :req-un [::profile-id ::id])) (sv/defmethod ::project + {::doc/added "1.0" + ::doc/deprecated "1.18"} [{:keys [pool]} {:keys [profile-id id]}] (with-open [conn (db/open pool)] (let [project (db/get-by-id conn :project id)] - (check-read-permissions! conn profile-id id) + (projects/check-read-permissions! conn profile-id id) project))) diff --git a/backend/test/backend_tests/rpc_font_test.clj b/backend/test/backend_tests/rpc_font_test.clj index bf27cba90..d1c3bdd60 100644 --- a/backend/test/backend_tests/rpc_font_test.clj +++ b/backend/test/backend_tests/rpc_font_test.clj @@ -9,6 +9,7 @@ [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] + [app.rpc :as-alias rpc] [app.storage :as sto] [backend-tests.helpers :as th] [clojure.test :as t] @@ -31,14 +32,14 @@ io/read-as-bytes) params {::th/type :create-font-variant - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :team-id team-id :font-id font-id :font-family "somefont" :font-weight 400 :font-style "normal" :data {"font/ttf" ttfdata}} - out (th/mutation! params)] + out (th/command! params)] (t/is (= 1 (:call-count @mock))) @@ -68,14 +69,14 @@ io/read-as-bytes) params {::th/type :create-font-variant - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :team-id team-id :font-id font-id :font-family "somefont" :font-weight 400 :font-style "normal" :data {"font/woff" data}} - out (th/mutation! params)] + out (th/command! params)] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -91,8 +92,3 @@ :font-family :font-weight :font-style)))) - - - - - diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 8e84b6a21..317d5a1d9 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -185,7 +185,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/query! :projects {:team-id team-id}) + (->> (rp/cmd! :get-projects {:team-id team-id}) (rx/map projects-fetched)))))) ;; --- EVENT: search @@ -671,7 +671,7 @@ {:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params)] - (->> (rp/mutation! :create-project params) + (->> (rp/cmd! :create-project params) (rx/tap on-success) (rx/map project-created) (rx/catch on-error)))))) @@ -733,7 +733,7 @@ (watch [_ state _] (let [project (get-in state [:dashboard-projects id]) params (select-keys project [:id :is-pinned :team-id])] - (->> (rp/mutation :update-project-pin params) + (->> (rp/cmd! :update-project-pin params) (rx/ignore)))))) ;; --- EVENT: rename-project @@ -751,7 +751,7 @@ ptk/WatchEvent (watch [_ _ _] (let [params {:id id :name name}] - (->> (rp/mutation :rename-project params) + (->> (rp/cmd! :rename-project params) (rx/ignore)))))) ;; --- EVENT: delete-project @@ -766,7 +766,7 @@ ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation :delete-project {:id id}) + (->> (rp/cmd! :delete-project {:id id}) (rx/ignore))))) ;; --- EVENT: delete-file @@ -1077,7 +1077,7 @@ action-name (if in-project? :create-file :create-project) action (if in-project? file-created project-created)] - (->> (rp/mutation! action-name params) + (->> (rp/cmd! action-name params) (rx/map action)))))) (defn open-selected-file diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index ff5ba2a52..b63c27304 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -267,7 +267,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/cmd! :get-project {:id project-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) @@ -1564,7 +1564,7 @@ (into (d/ordered-set))) undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) + (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (dws/select-shapes selected) (ptk/data-event :layout/update [frame-id]) diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 70f068e06..ccda8cca2 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -207,7 +207,7 @@ (mf/deps show?) (fn [] (when show? - (->> (rp/query! :all-projects) + (->> (rp/cmd! :get-all-projects) (rx/map group-by-team) (rx/subs #(when (mf/ref-val mounted-ref) (reset! teams %))))))) From ba2729fa4af125f84f35f2b008f83ad2d766f56a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Jan 2023 08:48:27 +0100 Subject: [PATCH 08/16] :sparkles: Move fonts queries and mutations to commands --- backend/src/app/rpc.clj | 1 + backend/src/app/rpc/commands/fonts.clj | 237 ++++++++++++++++++ backend/src/app/rpc/mutations/fonts.clj | 84 +------ frontend/src/app/main/data/fonts.cljs | 8 +- frontend/src/app/main/repo.cljs | 21 +- frontend/src/app/main/ui/dashboard/fonts.cljs | 2 +- 6 files changed, 264 insertions(+), 89 deletions(-) create mode 100644 backend/src/app/rpc/commands/fonts.clj diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 53dda5bdb..dd7a309ef 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -328,6 +328,7 @@ 'app.rpc.commands.audit 'app.rpc.commands.auth 'app.rpc.commands.feedback + 'app.rpc.commands.fonts 'app.rpc.commands.binfile 'app.rpc.commands.comments 'app.rpc.commands.demo diff --git a/backend/src/app/rpc/commands/fonts.clj b/backend/src/app/rpc/commands/fonts.clj new file mode 100644 index 000000000..abb3f2b22 --- /dev/null +++ b/backend/src/app/rpc/commands/fonts.clj @@ -0,0 +1,237 @@ +;; 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.fonts + (:require + [app.common.data :as d] + [app.common.exceptions :as ex] + [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.media :as media] + [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] + [app.rpc.commands.files :as files] + [app.rpc.commands.projects :as projects] + [app.rpc.commands.teams :as teams] + [app.rpc.doc :as-alias doc] + [app.rpc.helpers :as rph] + [app.rpc.quotes :as quotes] + [app.storage :as sto] + [app.util.services :as sv] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [promesa.core :as p] + [promesa.exec :as px])) + +(def valid-weight #{100 200 300 400 500 600 700 800 900 950}) +(def valid-style #{"normal" "italic"}) + +(s/def ::data (s/map-of ::us/string any?)) +(s/def ::file-id ::us/uuid) +(s/def ::font-id ::us/uuid) +(s/def ::id ::us/uuid) +(s/def ::name ::us/not-empty-string) +(s/def ::project-id ::us/uuid) +(s/def ::style valid-style) +(s/def ::team-id ::us/uuid) +(s/def ::team-id ::us/uuid) +(s/def ::weight valid-weight) + +;; --- QUERY: Get font variants + +(s/def ::get-font-variants + (s/and + (s/keys :req [::rpc/profile-id] + :opt-un [::team-id + ::file-id + ::project-id]) + (fn [o] + (or (contains? o :team-id) + (contains? o :file-id) + (contains? o :project-id))))) + +(sv/defmethod ::get-font-variants + {::doc/added "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id project-id] :as params}] + (with-open [conn (db/open pool)] + (cond + (uuid? team-id) + (do + (teams/check-read-permissions! conn profile-id team-id) + (db/query conn :team-font-variant + {:team-id team-id + :deleted-at nil})) + + (uuid? project-id) + (let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})] + (projects/check-read-permissions! conn profile-id project-id) + (db/query conn :team-font-variant + {:team-id (:team-id project) + :deleted-at nil})) + + (uuid? file-id) + (let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]}) + project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})] + (files/check-read-permissions! conn profile-id file-id) + (db/query conn :team-font-variant + {:team-id (:team-id project) + :deleted-at nil}))))) + + +(declare create-font-variant) + +(s/def ::create-font-variant + (s/keys :req [::rpc/profile-id] + :req-un [::team-id + ::data + ::font-id + ::font-family + ::font-weight + ::font-style])) + +(sv/defmethod ::create-font-variant + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] + (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] + (teams/check-edition-permissions! pool profile-id team-id) + (quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team + ::quotes/profile-id profile-id + ::quotes/team-id team-id}) + (create-font-variant cfg (assoc params :profile-id profile-id)))) + +(defn create-font-variant + [{:keys [::sto/storage ::db/pool ::wrk/executor ::rpc/climit]} {:keys [data] :as params}] + (letfn [(generate-fonts [data] + (climit/with-dispatch (:process-font climit) + (media/run {:cmd :generate-fonts :input data}))) + + ;; Function responsible of calculating cryptographyc hash of + ;; the provided data. + (calculate-hash [data] + (px/with-dispatch executor + (sto/calculate-hash data))) + + (validate-data [data] + (when (and (not (contains? data "font/otf")) + (not (contains? data "font/ttf")) + (not (contains? data "font/woff")) + (not (contains? data "font/woff2"))) + (ex/raise :type :validation + :code :invalid-font-upload)) + data) + + (persist-font-object [data mtype] + (when-let [resource (get data mtype)] + (p/let [hash (calculate-hash resource) + content (-> (sto/content resource) + (sto/wrap-with-hash hash))] + (sto/put-object! storage {::sto/content content + ::sto/touched-at (dt/now) + ::sto/deduplicate? true + :content-type mtype + :bucket "team-font-variant"})))) + + (persist-fonts [data] + (p/let [otf (persist-font-object data "font/otf") + ttf (persist-font-object data "font/ttf") + woff1 (persist-font-object data "font/woff") + woff2 (persist-font-object data "font/woff2")] + + (d/without-nils + {:otf otf + :ttf ttf + :woff1 woff1 + :woff2 woff2}))) + + (insert-into-db [{:keys [woff1 woff2 otf ttf]}] + (db/insert! pool :team-font-variant + {:id (uuid/next) + :team-id (:team-id params) + :font-id (:font-id params) + :font-family (:font-family params) + :font-weight (:font-weight params) + :font-style (:font-style params) + :woff1-file-id (:id woff1) + :woff2-file-id (:id woff2) + :otf-file-id (:id otf) + :ttf-file-id (:id ttf)})) + ] + + (->> (generate-fonts data) + (p/fmap validate-data) + (p/mcat executor persist-fonts) + (p/fmap executor insert-into-db) + (p/fmap (fn [result] + (let [params (update params :data (comp vec keys))] + (rph/with-meta result {::audit/replace-props params}))))))) + +;; --- UPDATE FONT FAMILY + +(s/def ::update-font + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::id ::name])) + +(sv/defmethod ::update-font + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id id name]}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + (rph/with-meta + (db/update! conn :team-font-variant + {:font-family name} + {:font-id id + :team-id team-id}) + {::audit/replace-props {:id id + :name name + :team-id team-id + :profile-id profile-id}}))) + +;; --- DELETE FONT + +(s/def ::delete-font + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::id])) + +(sv/defmethod ::delete-font + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + (let [font (db/update! conn :team-font-variant + {:deleted-at (dt/now)} + {:font-id id :team-id team-id})] + (rph/with-meta (rph/wrap) + {::audit/props {:id id + :team-id team-id + :name (:font-family font) + :profile-id profile-id}})))) + +;; --- DELETE FONT VARIANT + +(s/def ::delete-font-variant + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::id])) + +(sv/defmethod ::delete-font-variant + {::doc/added "1.18" + ::webhooks/event? true} + [{:keys [::db/pool]} {:keys [::rpc/profile-id id team-id]}] + (db/with-atomic [conn pool] + (teams/check-edition-permissions! conn profile-id team-id) + (let [variant (db/update! conn :team-font-variant + {:deleted-at (dt/now)} + {:id id :team-id team-id})] + (rph/with-meta (rph/wrap) + {::audit/props {:font-family (:font-family variant) + :font-id (:font-id variant)}})))) + diff --git a/backend/src/app/rpc/mutations/fonts.clj b/backend/src/app/rpc/mutations/fonts.clj index fa4d70069..a53bd9fd1 100644 --- a/backend/src/app/rpc/mutations/fonts.clj +++ b/backend/src/app/rpc/mutations/fonts.clj @@ -6,15 +6,12 @@ (ns app.rpc.mutations.fonts (:require - [app.common.data :as d] - [app.common.exceptions :as ex] [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.media :as media] - [app.rpc.climit :as-alias climit] + [app.rpc.commands.fonts :as fonts] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] @@ -22,10 +19,7 @@ [app.storage :as sto] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as-alias wrk] - [clojure.spec.alpha :as s] - [promesa.core :as p] - [promesa.exec :as px])) + [clojure.spec.alpha :as s])) (declare create-font-variant) @@ -45,8 +39,11 @@ (s/keys :req-un [::profile-id ::team-id ::data ::font-id ::font-family ::font-weight ::font-style])) +(declare create-font-variant) + (sv/defmethod ::create-font-variant {::doc/added "1.3" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}] (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] @@ -54,73 +51,7 @@ (quotes/check-quote! pool {::quotes/id ::quotes/font-variants-per-team ::quotes/profile-id profile-id ::quotes/team-id team-id}) - (create-font-variant cfg params))) - -(defn create-font-variant - [{:keys [::sto/storage ::db/pool ::wrk/executor climit] :as cfg} {:keys [data] :as params}] - (letfn [(generate-fonts [data] - (climit/with-dispatch (:process-font climit) - (media/run {:cmd :generate-fonts :input data}))) - - ;; Function responsible of calculating cryptographyc hash of - ;; the provided data. - (calculate-hash [data] - (px/with-dispatch executor - (sto/calculate-hash data))) - - (validate-data [data] - (when (and (not (contains? data "font/otf")) - (not (contains? data "font/ttf")) - (not (contains? data "font/woff")) - (not (contains? data "font/woff2"))) - (ex/raise :type :validation - :code :invalid-font-upload)) - data) - - (persist-font-object [data mtype] - (when-let [resource (get data mtype)] - (p/let [hash (calculate-hash resource) - content (-> (sto/content resource) - (sto/wrap-with-hash hash))] - (sto/put-object! storage {::sto/content content - ::sto/touched-at (dt/now) - ::sto/deduplicate? true - :content-type mtype - :bucket "team-font-variant"})))) - - (persist-fonts [data] - (p/let [otf (persist-font-object data "font/otf") - ttf (persist-font-object data "font/ttf") - woff1 (persist-font-object data "font/woff") - woff2 (persist-font-object data "font/woff2")] - - (d/without-nils - {:otf otf - :ttf ttf - :woff1 woff1 - :woff2 woff2}))) - - (insert-into-db [{:keys [woff1 woff2 otf ttf]}] - (db/insert! pool :team-font-variant - {:id (uuid/next) - :team-id (:team-id params) - :font-id (:font-id params) - :font-family (:font-family params) - :font-weight (:font-weight params) - :font-style (:font-style params) - :woff1-file-id (:id woff1) - :woff2-file-id (:id woff2) - :otf-file-id (:id otf) - :ttf-file-id (:id ttf)})) - ] - - (->> (generate-fonts data) - (p/fmap validate-data) - (p/mcat executor persist-fonts) - (p/fmap executor insert-into-db) - (p/fmap (fn [result] - (let [params (update params :data (comp vec keys))] - (rph/with-meta result {::audit/replace-props params}))))))) + (fonts/create-font-variant cfg params))) ;; --- UPDATE FONT FAMILY @@ -129,6 +60,7 @@ (sv/defmethod ::update-font {::doc/added "1.3" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [team-id profile-id id name] :as params}] (db/with-atomic [conn pool] @@ -150,6 +82,7 @@ (sv/defmethod ::delete-font {::doc/added "1.3" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}] (db/with-atomic [conn pool] @@ -170,6 +103,7 @@ (sv/defmethod ::delete-font-variant {::doc/added "1.3" + ::doc/deprecated "1.18" ::webhooks/event? true} [{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}] (db/with-atomic [conn pool] diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs index b9ac00443..cdfe3d36d 100644 --- a/frontend/src/app/main/data/fonts.cljs +++ b/frontend/src/app/main/data/fonts.cljs @@ -76,7 +76,7 @@ (ptk/reify ::load-team-fonts ptk/WatchEvent (watch [_ _ _] - (->> (rp/query :font-variants {:team-id team-id}) + (->> (rp/cmd! :get-font-variants {:team-id team-id}) (rx/map fonts-fetched))))) (defn process-upload @@ -236,7 +236,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/mutation! :update-font {:id id :name name :team-id team-id}) + (->> (rp/cmd! :update-font {:id id :name name :team-id team-id}) (rx/ignore)))))) (defn delete-font @@ -253,7 +253,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/mutation! :delete-font {:id font-id :team-id team-id}) + (->> (rp/cmd! :delete-font {:id font-id :team-id team-id}) (rx/ignore)))))) (defn delete-font-variant @@ -270,7 +270,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/mutation! :delete-font-variant {:id id :team-id team-id}) + (->> (rp/cmd! :delete-font-variant {:id id :team-id team-id}) (rx/ignore)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index be89b473e..f1bd5530e 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -12,18 +12,21 @@ [app.util.http :as http] [beicon.core :as rx])) +(derive :get-all-projects ::query) (derive :get-file ::query) -(derive :get-file-object-thumbnails ::query) -(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) -(derive :get-team-shared-files ::query) +(derive :get-file-libraries ::query) +(derive :get-file-object-thumbnails ::query) +(derive :get-font-variants ::query) (derive :get-profile ::query) +(derive :get-project ::query) +(derive :get-team-invitations ::query) +(derive :get-team-members ::query) +(derive :get-team-shared-files ::query) +(derive :get-team-stats ::query) +(derive :get-team-users ::query) +(derive :get-teams ::query) +(derive :search-files ::query) (defn handle-response [{:keys [status body] :as response}] diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index ffc657de6..461452268 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -94,7 +94,7 @@ (mf/deps team) (fn [item] (swap! uploading conj (:id item)) - (->> (rp/mutation! :create-font-variant item) + (->> (rp/cmd! :create-font-variant item) (rx/delay-at-least 2000) (rx/subs (fn [font] (swap! fonts dissoc (:id item)) From 6a699d7f09d211441770b90f39b3463d519cf9b1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Jan 2023 09:17:19 +0100 Subject: [PATCH 09/16] :sparkles: Properly move viewer queries to commands And change deprecation version on viewer queries --- backend/src/app/rpc.clj | 1 + backend/src/app/rpc/commands/viewer.clj | 2 +- backend/src/app/rpc/queries/viewer.clj | 2 +- frontend/src/app/main/data/viewer.cljs | 32 ++++++++++++------------- frontend/src/app/main/repo.cljs | 2 ++ 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index dd7a309ef..b0a44367d 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -344,6 +344,7 @@ 'app.rpc.commands.search 'app.rpc.commands.teams 'app.rpc.commands.verify-token + 'app.rpc.commands.viewer 'app.rpc.commands.webhooks) (map (partial process-method cfg)) (into {})))) diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index c19e84824..e9c1b0086 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]} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}] (with-open [conn (db/open pool)] (get-view-only-bundle conn (assoc params :profile-id profile-id)))) diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index 837dab94f..14a016096 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -22,7 +22,7 @@ (sv/defmethod ::view-only-bundle {::rpc/auth false ::doc/added "1.3" - ::doc/deprecated "1.17"} + ::doc/deprecated "1.18"} [{:keys [pool] :as cfg} {:keys [features components-v2] :as params}] (with-open [conn (db/open pool)] (let [;; BACKWARD COMPATIBILTY with the components-v2 parameter diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 4ae8f1263..9213cd938 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -129,22 +129,22 @@ (rx/map :content) (rx/map #(vector key %)))))] - (->> (rp/query! :view-only-bundle params') - (rx/mapcat - (fn [bundle] - (->> (rx/from (-> bundle :file :data :pages-index seq)) - (rx/merge-map - (fn [[_ page :as kp]] - (if (t/pointer? page) - (resolve kp) - (rx/of kp)))) - (rx/reduce conj {}) - (rx/map (fn [pages-index] - (update-in bundle [:file :data] assoc :pages-index pages-index)))))) - (rx/mapcat - (fn [{:keys [fonts] :as bundle}] - (rx/of (df/fonts-fetched fonts) - (bundle-fetched (merge bundle params)))))))))) + (->> (rp/cmd! :get-view-only-bundle params') + (rx/mapcat + (fn [bundle] + (->> (rx/from (-> bundle :file :data :pages-index seq)) + (rx/merge-map + (fn [[_ page :as kp]] + (if (t/pointer? page) + (resolve kp) + (rx/of kp)))) + (rx/reduce conj {}) + (rx/map (fn [pages-index] + (update-in bundle [:file :data] assoc :pages-index pages-index)))))) + (rx/mapcat + (fn [{:keys [fonts] :as bundle}] + (rx/of (df/fonts-fetched fonts) + (bundle-fetched (merge bundle params)))))))))) (declare go-to-frame-auto) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index f1bd5530e..320f5a682 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -13,6 +13,7 @@ [beicon.core :as rx])) (derive :get-all-projects ::query) +(derive :get-comment-threads ::query) (derive :get-file ::query) (derive :get-file-fragment ::query) (derive :get-file-libraries ::query) @@ -26,6 +27,7 @@ (derive :get-team-stats ::query) (derive :get-team-users ::query) (derive :get-teams ::query) +(derive :get-view-only-bundle ::query) (derive :search-files ::query) (defn handle-response From a79d2cf899231d1a8b62415ecc17ed4730dabffc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Jan 2023 09:15:52 +0100 Subject: [PATCH 10/16] :fire: Remove deprecated teams mutations and queries --- backend/src/app/rpc.clj | 2 - backend/src/app/rpc/mutations/teams.clj | 241 ------------------------ backend/src/app/rpc/queries/teams.clj | 87 --------- 3 files changed, 330 deletions(-) delete mode 100644 backend/src/app/rpc/mutations/teams.clj delete mode 100644 backend/src/app/rpc/queries/teams.clj diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index b0a44367d..1243f9364 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -300,7 +300,6 @@ (let [cfg (assoc cfg ::type "query" ::metrics-id :rpc-query-timing)] (->> (sv/scan-ns 'app.rpc.queries.projects - 'app.rpc.queries.teams 'app.rpc.queries.profile 'app.rpc.queries.viewer 'app.rpc.queries.fonts) @@ -314,7 +313,6 @@ 'app.rpc.mutations.media 'app.rpc.mutations.profile 'app.rpc.mutations.projects - 'app.rpc.mutations.teams 'app.rpc.mutations.fonts 'app.rpc.mutations.share-link) (map (partial process-method cfg)) diff --git a/backend/src/app/rpc/mutations/teams.clj b/backend/src/app/rpc/mutations/teams.clj deleted file mode 100644 index 92945fbfa..000000000 --- a/backend/src/app/rpc/mutations/teams.clj +++ /dev/null @@ -1,241 +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.teams - (:require - [app.common.exceptions :as ex] - [app.common.spec :as us] - [app.db :as db] - [app.emails :as eml] - [app.loggers.audit :as audit] - [app.media :as media] - [app.rpc.commands.teams :as cmd.teams] - [app.rpc.doc :as-alias doc] - [app.rpc.helpers :as rph] - [app.storage :as-alias sto] - [app.util.services :as sv] - [app.util.time :as dt] - [clojure.spec.alpha :as s] - [cuerdas.core :as str])) - -;; --- Helpers & Specs - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) -(s/def ::profile-id ::us/uuid) - -;; --- Mutation: Create Team - -(s/def ::create-team ::cmd.teams/create-team) - -(sv/defmethod ::create-team - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.teams/create-team conn params))) - -;; --- Mutation: Update Team - -(s/def ::update-team ::cmd.teams/update-team) - -(sv/defmethod ::update-team - {::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] - (cmd.teams/check-edition-permissions! conn profile-id id) - (db/update! conn :team - {:name name} - {:id id}) - nil)) - -;; --- Mutation: Leave Team - -(s/def ::leave-team ::cmd.teams/leave-team) - -(sv/defmethod ::leave-team - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.teams/leave-team conn params))) - -;; --- Mutation: Delete Team - -(s/def ::delete-team ::cmd.teams/delete-team) - -(sv/defmethod ::delete-team - {::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 (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}) - nil))) - - -;; --- Mutation: Team Update Role - -(s/def ::update-team-member-role ::cmd.teams/update-team-member-role) - -(sv/defmethod ::update-team-member-role - {::doc/added "1.0" - ::doc/deprecated "1.17"} - [{:keys [::db/pool] :as cfg} params] - (db/with-atomic [conn pool] - (cmd.teams/update-team-member-role conn params))) - -;; --- Mutation: Delete Team Member - -(s/def ::delete-team-member ::cmd.teams/delete-team-member) - -(sv/defmethod ::delete-team-member - {::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 (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)) - - (db/delete! conn :team-profile-rel {:profile-id member-id - :team-id team-id}) - - nil))) - -;; --- Mutation: Update Team Photo - -(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 ::sto/storage media/configure-assets-storage)] - (cmd.teams/update-team-photo cfg params))) - -;; --- Mutation: Invite Member - -(s/def ::invite-team-member ::cmd.teams/create-team-invitations) - -(sv/defmethod ::invite-team-member - {::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 (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))] - - (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 [cfg (assoc cfg ::cmd.teams/conn conn) - invitations (->> emails - (map (fn [email] - {: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-with-invitations) - -(sv/defmethod ::create-team-and-invite-members - {::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 (cmd.teams/create-team conn params) - profile (db/get-by-id conn :profile profile-id) - cfg (assoc cfg ::cmd.teams/conn conn)] - - ;; Create invitations for all provided emails. - (->> 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)}) - (rph/with-defer - #(when-let [collector (::audit/collector cfg)] - (audit/submit! collector - {:type "mutation" - :name "invite-team-member" - :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.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 (cmd.teams/get-permissions conn profile-id team-id)] - - (when-not (:is-admin perms) - (ex/raise :type :validation - :code :insufficient-permissions)) - - (db/update! conn :team-invitation - {:role (name role) :updated-at (dt/now)} - {:team-id team-id :email-to (str/lower email)}) - nil))) - -;; --- Mutation: Delete invitation - -(s/def ::delete-team-invitation ::cmd.teams/delete-team-invitation) - -(sv/defmethod ::delete-team-invitation - {::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 (cmd.teams/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/queries/teams.clj b/backend/src/app/rpc/queries/teams.clj deleted file mode 100644 index 10801413e..000000000 --- a/backend/src/app/rpc/queries/teams.clj +++ /dev/null @@ -1,87 +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.teams - (:require - [app.db :as db] - [app.rpc.commands.teams :as cmd.teams] - [app.rpc.doc :as-alias doc] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) - -;; --- Query: Teams - -(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)] - (cmd.teams/retrieve-teams conn profile-id))) - -;; --- Query: Team (by 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)] - (cmd.teams/retrieve-team conn profile-id id))) - -;; --- Query: Team Members - -(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)] - (cmd.teams/check-read-permissions! conn profile-id team-id) - (cmd.teams/retrieve-team-members conn team-id))) - -;; --- Query: Team Users -(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 - (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 - -(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)] - (cmd.teams/check-read-permissions! conn profile-id team-id) - (cmd.teams/retrieve-team-stats conn team-id))) - -;; --- Query: Team invitations - -(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)] - (cmd.teams/check-read-permissions! conn profile-id team-id) - (cmd.teams/get-team-invitations conn team-id))) From bca98f91e4d7ef3ca9564c9f0792d7164b2e76fd Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Jan 2023 10:47:05 +0100 Subject: [PATCH 11/16] :tada: Add rpc methods for access tokens --- backend/src/app/rpc/commands/access_token.clj | 25 ++++++++++++++++++- backend/test/backend_tests/helpers.clj | 6 +++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj index 48a10d128..9abf99c49 100644 --- a/backend/src/app/rpc/commands/access_token.clj +++ b/backend/src/app/rpc/commands/access_token.clj @@ -18,6 +18,12 @@ [app.util.time :as dt] [clojure.spec.alpha :as s])) +(defn- decode-row + [{:keys [perms] :as row}] + (cond-> row + (db/pgarray? perms "text") + (assoc :perms (db/decode-pgarray perms #{})))) + (defn- create-access-token [{:keys [::conn ::main/props]} profile-id name perms] (let [created-at (dt/now) @@ -58,7 +64,24 @@ (quotes/check-quote! conn {::quotes/id ::quotes/access-tokens-per-profile ::quotes/profile-id profile-id}) - (create-access-token cfg profile-id name perms)))) + (-> (create-access-token cfg profile-id name perms) + (decode-row))))) +(s/def ::delete-access-token + (s/keys :req [::rpc/profile-id] + :req-un [::us/id])) +(sv/defmethod ::delete-access-token + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id id]}] + (db/delete! pool :access-token {:id id :profile-id profile-id}) + nil) +(s/def ::get-access-tokens + (s/keys :req [::rpc/profile-id])) + +(sv/defmethod ::get-access-tokens + {::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [::rpc/profile-id]}] + (->> (db/query pool :access-token {:profile-id profile-id}) + (mapv decode-row))) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index c656d6c74..28d498486 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -8,6 +8,7 @@ (:require [app.auth] [app.common.data :as d] + [app.common.exceptions :as ex] [app.common.flags :as flags] [app.common.pages :as cp] [app.common.pprint :as pp] @@ -320,6 +321,11 @@ (defn command! [{:keys [::type] :as data}] (let [method-fn (get-in *system* [:app.rpc/methods :commands type])] + (when-not method-fn + (ex/raise :type :assertion + :code :rpc-method-not-found + :hint (str/ffmt "rpc method '%' not found" (name type)))) + ;; (app.common.pprint/pprint (:app.rpc/methods *system*)) (try-on! (method-fn (-> data (dissoc ::type) From 4258a840ac9d2d0dd1da15f0983bd8eecf2f5269 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 17 Jan 2023 10:58:45 +0100 Subject: [PATCH 12/16] :recycle: Use proper namespace qualified access to pool --- backend/src/app/rpc/commands/binfile.clj | 8 ++-- backend/src/app/rpc/commands/comments.clj | 26 +++++------ backend/src/app/rpc/commands/demo.clj | 2 +- backend/src/app/rpc/commands/files.clj | 44 +++++++++---------- backend/src/app/rpc/commands/files_create.clj | 2 +- backend/src/app/rpc/commands/files_temp.clj | 6 +-- backend/src/app/rpc/commands/files_update.clj | 2 +- backend/src/app/rpc/commands/ldap.clj | 2 +- backend/src/app/rpc/commands/management.clj | 10 ++--- backend/src/app/rpc/commands/media.clj | 2 +- backend/src/app/rpc/commands/projects.clj | 8 ++-- backend/src/app/rpc/commands/search.clj | 2 +- backend/src/app/rpc/commands/teams.clj | 18 ++++---- backend/src/app/rpc/commands/verify_token.clj | 2 +- backend/src/app/rpc/commands/webhooks.clj | 2 +- 15 files changed, 68 insertions(+), 68 deletions(-) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 10385915e..83182262f 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -476,7 +476,7 @@ [:v1/metadata :v1/files :v1/rels :v1/sobjects]))))) (defmethod write-section :v1/metadata - [{:keys [pool ::output ::file-ids ::include-libraries?]}] + [{:keys [::db/pool ::output ::file-ids ::include-libraries?]}] (let [libs (when include-libraries? (retrieve-libraries pool file-ids)) files (into file-ids libs)] @@ -484,7 +484,7 @@ (vswap! *state* assoc :files files))) (defmethod write-section :v1/files - [{:keys [pool ::output ::embed-assets?]}] + [{:keys [::db/pool ::output ::embed-assets?]}] ;; Initialize SIDS with empty vector (vswap! *state* assoc :sids []) @@ -508,7 +508,7 @@ (vswap! *state* update :sids into storage-object-id-xf media)))) (defmethod write-section :v1/rels - [{:keys [pool ::output ::include-libraries?]}] + [{:keys [::db/pool ::output ::include-libraries?]}] (let [rels (when include-libraries? (retrieve-library-relations pool (-> *state* deref :files)))] (l/debug :hint "found rels" :total (count rels) ::l/async false) @@ -920,7 +920,7 @@ "Import a penpot file in a binary format." {::doc/added "1.15" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}] (db/with-atomic [conn pool] (projects/check-read-permissions! conn profile-id project-id) (let [ids (import! (assoc cfg diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index f75dbf3b8..44366b894 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -100,7 +100,7 @@ (sv/defmethod ::get-comment-threads {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] (with-open [conn (db/open pool)] (files/check-comment-permissions! conn profile-id file-id share-id) (get-comment-threads conn profile-id file-id))) @@ -143,7 +143,7 @@ (sv/defmethod ::get-unread-comment-threads {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) (get-unread-comment-threads conn profile-id team-id))) @@ -190,7 +190,7 @@ (sv/defmethod ::get-comment-thread {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}] (with-open [conn (db/open pool)] (files/check-comment-permissions! conn profile-id file-id share-id) (let [sql (str "with threads as (" sql:comment-threads ")" @@ -210,7 +210,7 @@ (sv/defmethod ::get-comments {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}] (with-open [conn (db/open pool)] (let [{:keys [file-id] :as thread} (get-comment-thread conn thread-id)] (files/check-comment-permissions! conn profile-id file-id share-id) @@ -262,7 +262,7 @@ participants on comment threads of the file." {::doc/added "1.15" ::doc/changes ["1.15" "Imported from queries and renamed."]} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}] (with-open [conn (db/open pool)] (files/check-comment-permissions! conn profile-id file-id share-id) (get-file-comments-users conn file-id profile-id))) @@ -372,7 +372,7 @@ (sv/defmethod ::update-comment-thread-status {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) @@ -389,7 +389,7 @@ (sv/defmethod ::update-comment-thread {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}] (db/with-atomic [conn pool] (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) @@ -412,7 +412,7 @@ (sv/defmethod ::create-comment {::doc/added "1.15" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at thread-id share-id content] :as params}] (db/with-atomic [conn pool] (let [{:keys [file-id page-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true) {:keys [team-id project-id page-name] :as file} (get-file conn file-id page-id)] @@ -465,7 +465,7 @@ (sv/defmethod ::update-comment {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id ::rpc/request-at id share-id content] :as params}] (db/with-atomic [conn pool] (let [{:keys [thread-id] :as comment} (get-comment conn id ::db/for-update? true) {:keys [file-id page-id owner-id] :as thread} (get-comment-thread conn thread-id ::db/for-update? true)] @@ -498,7 +498,7 @@ (sv/defmethod ::delete-comment-thread {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] (let [{:keys [owner-id file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) @@ -518,7 +518,7 @@ (sv/defmethod ::delete-comment {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] (let [{:keys [owner-id thread-id] :as comment} (get-comment conn id ::db/for-update? true) {:keys [file-id] :as thread} (get-comment-thread conn thread-id)] @@ -538,7 +538,7 @@ (sv/defmethod ::update-comment-thread-position {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}] (db/with-atomic [conn pool] (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) @@ -558,7 +558,7 @@ (sv/defmethod ::update-comment-thread-frame {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}] (db/with-atomic [conn pool] (let [{:keys [file-id] :as thread} (get-comment-thread conn id ::db/for-update? true)] (files/check-comment-permissions! conn profile-id file-id share-id) diff --git a/backend/src/app/rpc/commands/demo.clj b/backend/src/app/rpc/commands/demo.clj index 1e1fd6bce..32897de92 100644 --- a/backend/src/app/rpc/commands/demo.clj +++ b/backend/src/app/rpc/commands/demo.clj @@ -29,7 +29,7 @@ {::rpc/auth false ::doc/added "1.15" ::doc/changes ["1.15" "This method is migrated from mutations to commands."]} - [{:keys [pool] :as cfg} _] + [{:keys [::db/pool] :as cfg} _] (when-not (contains? cf/flags :demo-users) (ex/raise :type :validation diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index a7067ee1c..054e15d89 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -258,7 +258,7 @@ (handle-file-features client-features)))) (defn get-minimal-file - [{:keys [pool] :as cfg} id] + [{:keys [::db/pool] :as cfg} id] (db/get pool :file {:id id} {:columns [:id :modified-at :revn]})) (defn get-file-etag @@ -275,7 +275,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]}] + [{:keys [::db/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) @@ -303,7 +303,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] }] + [{:keys [::db/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) @@ -339,7 +339,7 @@ ::cond/get-object #(get-minimal-file %1 (:file-id %2)) ::cond/reuse-key? true ::cond/key-fn get-file-etag} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (get-object-thumbnails conn file-id))) @@ -370,7 +370,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]}] + [{:keys [::db/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))) @@ -389,7 +389,7 @@ (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]}] + [{:keys [::db/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 file-id))) @@ -456,7 +456,7 @@ Mainly used for rendering purposes." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (get-page conn params))) @@ -508,7 +508,7 @@ (sv/defmethod ::get-team-shared-files "Get all file (libraries) for the specified team." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) (get-team-shared-files conn team-id))) @@ -562,7 +562,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]}] + [{:keys [::db/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))) @@ -588,7 +588,7 @@ (sv/defmethod ::get-library-file-references "Returns all the file references that use specified file (library) id." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (get-library-file-references conn file-id))) @@ -625,7 +625,7 @@ (sv/defmethod ::get-team-recent-files {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) (get-team-recent-files conn team-id))) @@ -659,7 +659,7 @@ (sv/defmethod ::get-file-thumbnail {::doc/added "1.17"} - [{:keys [pool]} {:keys [::rpc/profile-id file-id revn]}] + [{:keys [::db/pool]} {:keys [::rpc/profile-id file-id revn]}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (-> (get-file-thumbnail conn file-id revn) @@ -755,7 +755,7 @@ "Retrieves the data for generate the thumbnail of the file. Used mainly for render thumbnails on dashboard." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as props}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) ;; NOTE: we force here the "storage/pointer-map" feature, because @@ -787,7 +787,7 @@ (sv/defmethod ::rename-file {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) (let [file (rename-file conn params)] @@ -836,7 +836,7 @@ (sv/defmethod ::set-file-shared {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-shared] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id is-shared] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) (when-not is-shared @@ -865,7 +865,7 @@ (sv/defmethod ::delete-file {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) (absorb-library conn params) @@ -895,7 +895,7 @@ (sv/defmethod ::link-file-to-library {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}] (when (= file-id library-id) (ex/raise :type :validation :code :invalid-library @@ -920,7 +920,7 @@ (sv/defmethod ::unlink-file-from-library {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (unlink-file-from-library conn params))) @@ -944,7 +944,7 @@ (sv/defmethod ::update-file-library-sync-status "Update the synchronization statos of a file->library link" {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (update-sync conn params))) @@ -966,7 +966,7 @@ (sv/defmethod ::ignore-file-library-sync-status "Ignore updates in linked files" {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (-> (ignore-sync conn params) @@ -996,7 +996,7 @@ (sv/defmethod ::upsert-file-object-thumbnail {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (upsert-file-object-thumbnail! conn params) @@ -1026,7 +1026,7 @@ "Creates or updates the file thumbnail. Mainly used for paint the grid thumbnails." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) (upsert-file-thumbnail conn params) diff --git a/backend/src/app/rpc/commands/files_create.clj b/backend/src/app/rpc/commands/files_create.clj index c6e904672..772d7aa25 100644 --- a/backend/src/app/rpc/commands/files_create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -80,7 +80,7 @@ (sv/defmethod ::create-file {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] (db/with-atomic [conn pool] (projects/check-edition-permissions! conn profile-id project-id) (let [team-id (files/get-team-id conn project-id) diff --git a/backend/src/app/rpc/commands/files_temp.clj b/backend/src/app/rpc/commands/files_temp.clj index ed3405d17..b0982c4a1 100644 --- a/backend/src/app/rpc/commands/files_temp.clj +++ b/backend/src/app/rpc/commands/files_temp.clj @@ -37,7 +37,7 @@ (sv/defmethod ::create-temp-file {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] (db/with-atomic [conn pool] (projects/check-edition-permissions! conn profile-id project-id) (create-file conn (assoc params :profile-id profile-id :deleted-at (dt/in-future {:days 1}))))) @@ -65,7 +65,7 @@ (sv/defmethod ::update-temp-file {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (update-temp-file conn (assoc params :profile-id profile-id)) nil)) @@ -102,7 +102,7 @@ (sv/defmethod ::persist-temp-file {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id id) (persist-temp-file conn params))) diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index 89c39ff04..d48d609e6 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -132,7 +132,7 @@ ::webhooks/batch-timeout (dt/duration "2m") ::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}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id id) (db/xact-lock! conn id) diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index 921e7b4c7..e434f537c 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -77,7 +77,7 @@ ::audit/profile-id (:id profile)})))))) (defn- login-or-register - [{:keys [pool] :as cfg} info] + [{:keys [::db/pool] :as cfg} info] (db/with-atomic [conn pool] (or (some->> (:email info) (profile/get-profile-by-email conn) diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index e0ed3d2d6..515698118 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -46,7 +46,7 @@ "Duplicate a single file in the same team." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (duplicate-file conn (assoc params :profile-id profile-id)))) @@ -221,7 +221,7 @@ "Duplicate an entire project with all the files" {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} params] + [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] (duplicate-project conn (assoc params :profile-id (::rpc/profile-id params))))) @@ -329,7 +329,7 @@ "Move a set of files from one project to other." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (move-files conn (assoc params :profile-id profile-id)))) @@ -369,7 +369,7 @@ "Move projects between teams." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (move-project conn (assoc params :profile-id profile-id)))) @@ -386,7 +386,7 @@ "Clone into the specified project the template by its id." {::doc/added "1.16" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (-> (assoc cfg :conn conn) (clone-template (assoc params :profile-id profile-id))))) diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index f7853baa4..168a78538 100644 --- a/backend/src/app/rpc/commands/media.clj +++ b/backend/src/app/rpc/commands/media.clj @@ -254,7 +254,7 @@ (sv/defmethod ::clone-file-media-object {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] + [{:keys [::db/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) diff --git a/backend/src/app/rpc/commands/projects.clj b/backend/src/app/rpc/commands/projects.clj index 518aec84a..f9555479d 100644 --- a/backend/src/app/rpc/commands/projects.clj +++ b/backend/src/app/rpc/commands/projects.clj @@ -174,7 +174,7 @@ (sv/defmethod ::create-project {::doc/added "1.18" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] (db/with-atomic [conn pool] (teams/check-edition-permissions! conn profile-id team-id) (quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team @@ -212,7 +212,7 @@ ::webhooks/batch-timeout (dt/duration "5s") ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id) ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) (db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned]) @@ -229,7 +229,7 @@ (sv/defmethod ::rename-project {::doc/added "1.18" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) (let [project (db/get-by-id conn :project id ::db/for-update? true)] @@ -253,7 +253,7 @@ (sv/defmethod ::delete-project {::doc/added "1.18" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) (let [project (db/update! conn :project diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj index 5f1ddc559..6fb4ea112 100644 --- a/backend/src/app/rpc/commands/search.clj +++ b/backend/src/app/rpc/commands/search.clj @@ -64,5 +64,5 @@ (sv/defmethod ::search-files {::doc/added "1.17"} - [{:keys [pool]} {:keys [::rpc/profile-id team-id search-term]}] + [{:keys [::db/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 f70aa3d01..3662b935b 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -83,7 +83,7 @@ (sv/defmethod ::get-teams {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (with-open [conn (db/open pool)] (retrieve-teams conn profile-id))) @@ -128,7 +128,7 @@ (sv/defmethod ::get-team {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}] (with-open [conn (db/open pool)] (retrieve-team conn profile-id id))) @@ -440,7 +440,7 @@ (sv/defmethod ::leave-team {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (leave-team conn (assoc params :profile-id profile-id)))) @@ -456,7 +456,7 @@ (sv/defmethod ::delete-team {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id id)] (when-not (:is-owner perms) @@ -552,7 +552,7 @@ (sv/defmethod ::delete-team-member {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-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) @@ -706,7 +706,7 @@ "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 [::rpc/profile-id team-id email emails role] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/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) @@ -755,7 +755,7 @@ (sv/defmethod ::create-team-with-invitations {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/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) @@ -826,7 +826,7 @@ (sv/defmethod ::update-team-invitation-role {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] @@ -847,7 +847,7 @@ (sv/defmethod ::delete-team-invitation {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 914116ee9..e00db2c5e 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -35,7 +35,7 @@ (sv/defmethod ::verify-token {::rpc/auth false ::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [token] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [token] :as params}] (db/with-atomic [conn pool] (let [claims (tokens/verify (::main/props cfg) {:token token}) cfg (assoc cfg :conn conn)] diff --git a/backend/src/app/rpc/commands/webhooks.clj b/backend/src/app/rpc/commands/webhooks.clj index b08b3443b..f6ea56c2e 100644 --- a/backend/src/app/rpc/commands/webhooks.clj +++ b/backend/src/app/rpc/commands/webhooks.clj @@ -140,7 +140,7 @@ from webhook where team_id = ? order by uri") (sv/defmethod ::get-webhooks - [{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id team-id) (db/exec! conn [sql:get-webhooks team-id]))) From 1e1f55138391fca86b86cef64e913446b8e5ffd8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 18 Jan 2023 10:48:58 +0100 Subject: [PATCH 13/16] :sparkles: Move share link mutations to commands --- backend/src/app/rpc.clj | 1 + backend/src/app/rpc/commands/files.clj | 5 +- backend/src/app/rpc/commands/files_share.clj | 71 ++++++++++++++++++++ backend/src/app/rpc/commands/viewer.clj | 6 +- backend/src/app/rpc/mutations/share_link.clj | 15 +++-- backend/src/app/rpc/queries/share_link.clj | 22 ------ frontend/src/app/main/data/common.cljs | 4 +- 7 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 backend/src/app/rpc/commands/files_share.clj delete mode 100644 backend/src/app/rpc/queries/share_link.clj diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 1243f9364..15f5b8b40 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -332,6 +332,7 @@ 'app.rpc.commands.demo 'app.rpc.commands.files 'app.rpc.commands.files-create + 'app.rpc.commands.files-share 'app.rpc.commands.files-temp 'app.rpc.commands.files-update 'app.rpc.commands.ldap diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 054e15d89..a0eee2288 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -28,7 +28,6 @@ [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.permissions :as perms] - [app.rpc.queries.share-link :refer [retrieve-share-link]] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.services :as sv] @@ -128,7 +127,9 @@ ([conn profile-id file-id share-id] (let [perms (get-permissions conn profile-id file-id) - ldata (retrieve-share-link conn file-id share-id)] + ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id}) + (dissoc :flags) + (update :pages db/decode-pgarray #{}))] ;; NOTE: in a future when share-link becomes more powerful and ;; will allow us specify which parts of the app is available, we diff --git a/backend/src/app/rpc/commands/files_share.clj b/backend/src/app/rpc/commands/files_share.clj new file mode 100644 index 000000000..f517fbb1f --- /dev/null +++ b/backend/src/app/rpc/commands/files_share.clj @@ -0,0 +1,71 @@ +;; 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.files-share + "Share link related rpc mutation methods." + (:require + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.db :as db] + [app.rpc :as-alias rpc] + [app.rpc.commands.files :as files] + [app.rpc.doc :as-alias doc] + [app.util.services :as sv] + [clojure.spec.alpha :as s])) + +;; --- Helpers & Specs + +(s/def ::file-id ::us/uuid) +(s/def ::who-comment ::us/string) +(s/def ::who-inspect ::us/string) +(s/def ::pages (s/every ::us/uuid :kind set?)) + +;; --- MUTATION: Create Share Link + +(declare create-share-link) + +(s/def ::create-share-link + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::who-comment ::who-inspect ::pages])) + +(sv/defmethod ::create-share-link + "Creates a share-link object. + + Share links are resources that allows external users access to specific + pages of a file with specific permissions (who-comment and who-inspect)." + {::doc/added "1.18"} + [{:keys [::db/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) + (create-share-link conn (assoc params :profile-id profile-id)))) + +(defn create-share-link + [conn {:keys [profile-id file-id pages who-comment who-inspect]}] + (let [pages (db/create-array conn "uuid" pages) + slink (db/insert! conn :share-link + {:id (uuid/next) + :file-id file-id + :who-comment who-comment + :who-inspect who-inspect + :pages pages + :owner-id profile-id})] + + (update slink :pages db/decode-pgarray #{}))) + +;; --- MUTATION: Delete Share Link + +(s/def ::delete-share-link + (s/keys :req [::rpc/profile-id] + :req-un [::us/id])) + +(sv/defmethod ::delete-share-link + {::doc/added "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] + (db/with-atomic [conn pool] + (let [slink (db/get-by-id conn :share-link id)] + (files/check-edition-permissions! conn profile-id (:file-id slink)) + (db/delete! conn :share-link {:id id}) + nil))) diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index e9c1b0086..136bc309b 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -13,11 +13,10 @@ [app.rpc.commands.files :as files] [app.rpc.cond :as-alias cond] [app.rpc.doc :as-alias doc] - [app.rpc.queries.share-link :as slnk] [app.util.services :as sv] [clojure.spec.alpha :as s])) -;; --- Query: View Only Bundle +;; --- QUERY: View Only Bundle (defn- get-project [conn id] @@ -31,7 +30,8 @@ users (comments/get-file-comments-users conn file-id profile-id) links (->> (db/query conn :share-link {:file-id file-id}) - (mapv slnk/decode-share-link-row)) + (mapv (fn [row] + (update row :pages db/decode-pgarray #{})))) fonts (db/query conn :team-font-variant {:team-id (:team-id project) diff --git a/backend/src/app/rpc/mutations/share_link.clj b/backend/src/app/rpc/mutations/share_link.clj index 9e8ab45d6..365aa77bd 100644 --- a/backend/src/app/rpc/mutations/share_link.clj +++ b/backend/src/app/rpc/mutations/share_link.clj @@ -11,6 +11,7 @@ [app.common.uuid :as uuid] [app.db :as db] [app.rpc.commands.files :as files] + [app.rpc.doc :as-alias doc] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -35,8 +36,9 @@ Share links are resources that allows external users access to specific pages of a file with specific permissions (who-comment and who-inspect)." - - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + {::doc/added "1.5" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) (create-share-link conn params))) @@ -51,18 +53,17 @@ :who-inspect who-inspect :pages pages :owner-id profile-id})] - (-> slink - (update :pages db/decode-pgarray #{})))) + (update slink :pages db/decode-pgarray #{}))) ;; --- Mutation: Delete Share Link -(declare delete-share-link) - (s/def ::delete-share-link (s/keys :req-un [::profile-id ::id])) (sv/defmethod ::delete-share-link - [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] + {::doc/added "1.5" + ::doc/deprecated "1.18"} + [{:keys [::db/pool] :as cfg} {:keys [profile-id id] :as params}] (db/with-atomic [conn pool] (let [slink (db/get-by-id conn :share-link id)] (files/check-edition-permissions! conn profile-id (:file-id slink)) diff --git a/backend/src/app/rpc/queries/share_link.clj b/backend/src/app/rpc/queries/share_link.clj deleted file mode 100644 index 8272e991f..000000000 --- a/backend/src/app/rpc/queries/share_link.clj +++ /dev/null @@ -1,22 +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.share-link - (:require - [app.db :as db])) - -(defn decode-share-link-row - [row] - (-> row - (dissoc :flags) - (update :pages db/decode-pgarray #{}))) - -(defn retrieve-share-link - [conn file-id share-id] - (some-> (db/get* conn :share-link - {:id share-id :file-id file-id}) - (decode-share-link-row))) - diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index e5072d13c..2b10703a4 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -27,7 +27,7 @@ (ptk/reify ::create-share-link ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation! :create-share-link params) + (->> (rp/cmd! :create-share-link params) (rx/map share-link-created))))) (defn delete-share-link @@ -41,6 +41,6 @@ ptk/WatchEvent (watch [_ _ _] - (->> (rp/mutation! :delete-share-link {:id id}) + (->> (rp/cmd! :delete-share-link {:id id}) (rx/ignore))))) From 3dd65db6510e108fc7da112f30d13064290d8292 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 18 Jan 2023 11:20:36 +0100 Subject: [PATCH 14/16] :sparkles: Use commands instead of mutations for assets upload And properly deprecate media rpc mutations --- backend/src/app/rpc/mutations/media.clj | 6 +++--- frontend/src/app/main/data/workspace.cljs | 2 +- frontend/src/app/main/data/workspace/media.cljs | 4 ++-- frontend/src/app/worker/import.cljs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index 964cf37f8..c547f22e4 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -21,7 +21,7 @@ (sv/defmethod ::upload-file-media-object {::doc/added "1.2" - ::doc/deprecated "1.17"} + ::doc/deprecated "1.18"} [{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}] (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) @@ -35,7 +35,7 @@ (sv/defmethod ::create-file-media-object-from-url {::doc/added "1.3" - ::doc/deprecated "1.17"} + ::doc/deprecated "1.18"} [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] (files/check-edition-permissions! pool profile-id file-id) @@ -47,7 +47,7 @@ (sv/defmethod ::clone-file-media-object {::doc/added "1.2" - ::doc/deprecated "1.17"} + ::doc/deprecated "1.18"} [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index b63c27304..cadb87e44 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1427,7 +1427,7 @@ :file-id file-id :content blob :is-local true})) - (rx/mapcat #(rp/mutation! :upload-file-media-object %)) + (rx/mapcat #(rp/cmd! :upload-file-media-object %)) (rx/map (fn [media] (assoc media :prev-id (:id imgpart)))))) diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 8fd501f59..a0131dca5 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -125,7 +125,7 @@ (rx/map dmm/validate-file) (rx/filter (comp not svg-blob?)) (rx/map prepare-blob) - (rx/mapcat #(rp/mutation! :upload-file-media-object %)) + (rx/mapcat #(rp/cmd! :upload-file-media-object %)) (rx/do on-image)) (->> (rx/from blobs) @@ -361,7 +361,7 @@ :type :info :timeout nil :tag :media-loading})) - (->> (rp/mutation! :clone-file-media-object params) + (->> (rp/cmd! :clone-file-media-object params) (rx/do on-success) (rx/catch on-error) (rx/finalize #(st/emit! (dm/hide-tag :media-loading))))))))) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 05e34b585..49036a518 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -196,7 +196,7 @@ :content blob :is-local true})) (rx/tap #(progress! context :upload-media name)) - (rx/flat-map #(rp/mutation! :upload-file-media-object %)))) + (rx/flat-map #(rp/cmd! :upload-file-media-object %)))) (defn resolve-text-content [node context] (let [resolve (:resolve context)] @@ -511,7 +511,7 @@ :content content :is-local false}))) (rx/tap #(progress! context :upload-media (:name %))) - (rx/merge-map #(rp/mutation! :upload-file-media-object %)) + (rx/merge-map #(rp/cmd! :upload-file-media-object %)) (rx/map (constantly media)) (rx/catch #(do (.error js/console (str "Error uploading media: " (:name media)) ) (rx/empty))))))) From 8a5afefc1c5c7fc9fd8a0724bf123c7b2049b28d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 18 Jan 2023 17:12:33 +0100 Subject: [PATCH 15/16] :tada: Add prepl support And rename the current repl to urepl (user-repl). --- backend/scripts/repl | 5 ++++ backend/src/app/config.clj | 14 +++++---- backend/src/app/main.clj | 11 +++++-- backend/src/app/srepl.clj | 61 ++++++++++++++++++++++++++------------ 4 files changed, 63 insertions(+), 28 deletions(-) diff --git a/backend/scripts/repl b/backend/scripts/repl index 26bf66b88..d253345ee 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -10,6 +10,11 @@ export PENPOT_FLAGS="\ enable-demo-users \ disable-secure-session-cookies \ enable-smtp \ + enable-prepl-server \ + enable-urepl-server \ + enable-rpc-climit \ + enable-rpc-rlimit \ + enable-soft-rpc-rlimit \ enable-webhooks \ enable-access-tokens"; diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 3ce625f70..2129525ba 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -64,8 +64,6 @@ :tenant "default" :redis-uri "redis://redis/0" - :srepl-host "127.0.0.1" - :srepl-port 6062 :assets-storage-backend :assets-fs :storage-assets-fs-directory "assets" @@ -197,8 +195,10 @@ (s/def ::smtp-ssl ::us/boolean) (s/def ::smtp-tls ::us/boolean) (s/def ::smtp-username (s/nilable ::us/string)) -(s/def ::srepl-host ::us/string) -(s/def ::srepl-port ::us/integer) +(s/def ::urepl-host ::us/string) +(s/def ::urepl-port ::us/integer) +(s/def ::prepl-host ::us/string) +(s/def ::prepl-port ::us/integer) (s/def ::assets-storage-backend ::us/keyword) (s/def ::storage-assets-fs-directory ::us/string) (s/def ::storage-assets-s3-bucket ::us/string) @@ -310,8 +310,10 @@ ::smtp-tls ::smtp-username - ::srepl-host - ::srepl-port + ::urepl-host + ::urepl-port + ::prepl-host + ::prepl-port ::assets-storage-backend ::storage-assets-fs-directory diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index f6fdaebbf..d7f0e72ea 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -30,6 +30,7 @@ [app.redis :as-alias rds] [app.rpc :as-alias rpc] [app.rpc.doc :as-alias rpc.doc] + [app.srepl :as-alias srepl] [app.storage :as-alias sto] [app.util.time :as dt] [app.worker :as-alias wrk] @@ -414,9 +415,13 @@ ::http.client/client (ig/ref ::http.client/client) ::props (ig/ref :app.setup/props)} - :app.srepl/server - {:port (cf/get :srepl-port) - :host (cf/get :srepl-host)} + [::srepl/urepl ::srepl/server] + {:port (cf/get :urepl-port 6062) + :host (cf/get :urepl-host "localhost")} + + [::srepl/prepl ::srepl/server] + {:port (cf/get :prepl-port 6063) + :host (cf/get :prepl-host "localhost")} :app.setup/builtin-templates {::http.client/client (ig/ref ::http.client/client)} diff --git a/backend/src/app/srepl.clj b/backend/src/app/srepl.clj index ed4b44467..cd5838a76 100644 --- a/backend/src/app/srepl.clj +++ b/backend/src/app/srepl.clj @@ -9,7 +9,10 @@ (:require [app.common.logging :as l] [app.common.spec :as us] + [app.config :as cf] [app.srepl.main] + [app.util.json :as json] + [app.util.locks :as locks] [clojure.core.server :as ccs] [clojure.main :as cm] [clojure.spec.alpha :as s] @@ -20,39 +23,59 @@ (ccs/repl-init) (in-ns 'app.srepl.main)) -(defn repl +(defn user-repl [] (cm/repl :init repl-init :read ccs/repl-read)) +(defn json-prepl + [] + (let [out *out* + lock (locks/create)] + (ccs/prepl *in* + (fn [m] + (binding [*out* out, *flush-on-newline* true, *print-readably* true] + (locks/locking lock + (println (json/encode-str m)))))))) + ;; --- State initialization -(s/def ::name ::us/not-empty-string) -(s/def ::port int?) +(s/def ::port ::us/integer) (s/def ::host ::us/not-empty-string) +(s/def ::flag #{:urepl-server :prepl-server}) +(s/def ::type #{::prepl ::urepl}) +(s/def ::key (s/tuple ::type ::us/keyword)) (defmethod ig/pre-init-spec ::server [_] - (s/keys :opt-un [::port ::host ::name])) + (s/keys :req [::flag] + :req-un [::port ::host])) (defmethod ig/prep-key ::server - [_ cfg] - (merge {:name "main"} cfg)) + [[type _] cfg] + (assoc cfg ::flag (keyword (str (name type) "-server")))) (defmethod ig/init-key ::server - [_ {:keys [port host name] :as cfg}] - (when (and port host name) - (l/info :msg "initializing server repl" :port port :host host :name name) - (ccs/start-server {:address host - :port port - :name name - :accept 'app.srepl/repl}) - cfg)) + [[type _] {:keys [::flag port host] :as cfg}] + (when (contains? cf/flags flag) + (let [accept (case type + ::prepl 'app.srepl/json-prepl + ::urepl 'app.srepl/user-repl) + params {:address host + :port port + :name (name type) + :accept accept}] + + (l/info :msg "initializing repl server" + :name (name type) + :port port + :host host) + + (ccs/start-server params) + + params))) (defmethod ig/halt-key! ::server - [_ cfg] - (when cfg - (ccs/stop-server (:name cfg)))) - - + [_ params] + (some-> params :name ccs/stop-server)) From 9a407ab714a1d91cf90c16980f3341a9802e0a41 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 19 Jan 2023 12:41:06 +0100 Subject: [PATCH 16/16] :tada: Add namespace with a set of helpers for access throught the BREPL --- backend/src/app/srepl.clj | 1 + backend/src/app/srepl/ext.clj | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 backend/src/app/srepl/ext.clj diff --git a/backend/src/app/srepl.clj b/backend/src/app/srepl.clj index cd5838a76..5885fa369 100644 --- a/backend/src/app/srepl.clj +++ b/backend/src/app/srepl.clj @@ -10,6 +10,7 @@ [app.common.logging :as l] [app.common.spec :as us] [app.config :as cf] + [app.srepl.ext] [app.srepl.main] [app.util.json :as json] [app.util.locks :as locks] diff --git a/backend/src/app/srepl/ext.clj b/backend/src/app/srepl/ext.clj new file mode 100644 index 000000000..640c6ee47 --- /dev/null +++ b/backend/src/app/srepl/ext.clj @@ -0,0 +1,42 @@ +;; 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.srepl.ext + "PREPL API for external usage (CLI or ADMIN)" + (:require + [app.auth :as auth] + [app.common.uuid :as uuid] + [app.db :as db] + [app.rpc.commands.auth :as cmd.auth])) + +(defn- get-current-system + [] + (or (deref (requiring-resolve 'app.main/system)) + (deref (requiring-resolve 'user/system)))) + +(defn derive-password + [password] + (auth/derive-password password)) + +(defn create-profile + [fullname, email, password] + (when-let [system (get-current-system)] + (db/with-atomic [conn (:app.db/pool system)] + (let [params {:id (uuid/next) + :email email + :fullname fullname + :is-active true + :password (derive-password password) + :props {}} + profile (->> (cmd.auth/create-profile! conn params) + (cmd.auth/create-profile-rels! conn))] + (str (:id profile)))))) + + + + + +