diff --git a/backend/scripts/repl b/backend/scripts/repl index e6b3c6f24..3fe39461b 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -2,7 +2,7 @@ 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" +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_DATABASE_URI="postgresql://172.17.0.1:5432/penpot" # export PENPOT_DATABASE_USERNAME="penpot" diff --git a/backend/scripts/start-dev b/backend/scripts/start-dev index d49c7c137..86edb2d70 100755 --- a/backend/scripts/start-dev +++ b/backend/scripts/start-dev @@ -2,7 +2,7 @@ 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" +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" set -ex diff --git a/backend/src/app/auth.clj b/backend/src/app/auth.clj new file mode 100644 index 000000000..cabe859f3 --- /dev/null +++ b/backend/src/app/auth.clj @@ -0,0 +1,26 @@ +;; 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.auth + (:require + [buddy.hashers :as hashers])) + +(defn derive-password + [password] + (hashers/derive password + {:alg :argon2id + :memory 16384 + :iterations 20 + :parallelism 2})) + +(defn verify-password + [attempt password] + (try + (hashers/verify attempt password) + (catch Throwable _ + {:update false + :valid false}))) + diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 95a221b3a..1cb1b2aca 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -106,6 +106,9 @@ (s/def ::file-change-snapshot-every ::us/integer) (s/def ::file-change-snapshot-timeout ::dt/duration) +(s/def ::setup-admin-email ::us/email) +(s/def ::setup-admin-password ::us/not-empty-string) + (s/def ::default-executor-parallelism ::us/integer) (s/def ::scheduled-executor-parallelism ::us/integer) @@ -295,6 +298,9 @@ ::srepl-host ::srepl-port + ::setup-admin-email + ::setup-admin-password + ::assets-storage-backend ::storage-assets-fs-directory ::storage-assets-s3-bucket diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index d91770348..b3d543108 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -290,7 +290,11 @@ {:pool (ig/ref ::db/pool) :executor (ig/ref ::wrk/executor) :storage (ig/ref ::sto/storage) - :session (ig/ref :app.http.session/manager)} + :session (ig/ref :app.http.session/manager) + + ::db/pool (ig/ref ::db/pool) + ::wrk/executor (ig/ref ::wrk/executor) + ::sto/storage (ig/ref ::sto/storage)} :app.http.websocket/handler {:pool (ig/ref ::db/pool) @@ -385,8 +389,8 @@ :max-age cf/deletion-delay} :app.tasks.objects-gc/handler - {:pool (ig/ref ::db/pool) - :storage (ig/ref ::sto/storage)} + {::db/pool (ig/ref ::db/pool) + ::sto/storage (ig/ref ::sto/storage)} :app.tasks.file-gc/handler {:pool (ig/ref ::db/pool)} @@ -403,6 +407,9 @@ {:port (cf/get :srepl-port) :host (cf/get :srepl-host)} + :app.setup/initial-profile + {::db/pool (ig/ref ::db/pool)} + :app.setup/builtin-templates {::http.client/client (ig/ref ::http.client/client)} diff --git a/backend/src/app/metrics.clj b/backend/src/app/metrics.clj index aa5979c4a..c11d3d112 100644 --- a/backend/src/app/metrics.clj +++ b/backend/src/app/metrics.clj @@ -132,7 +132,7 @@ (defmethod run-collector! :counter [{:keys [::mdef/instance]} {:keys [inc labels] :or {inc 1 labels default-empty-labels}}] - (let [instance (.labels instance (if (is-array? labels) labels (into-array String labels)))] + (let [instance (.labels ^Counter instance (if (is-array? labels) labels (into-array String labels)))] (.inc ^Counter$Child instance (double inc)))) (defmethod run-collector! :gauge diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index f659d93ee..d8b13bcbb 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -271,6 +271,37 @@ {:name "0087-mod-task-table" :fn (mg/resource "app/migrations/sql/0087-mod-task-table.sql")} + + {:name "0088-mod-team-profile-rel-table" + :fn (mg/resource "app/migrations/sql/0088-mod-team-profile-rel-table.sql")} + + {:name "0089-mod-project-profile-rel-table" + :fn (mg/resource "app/migrations/sql/0089-mod-project-profile-rel-table.sql")} + + {:name "0090-mod-http-session-table" + :fn (mg/resource "app/migrations/sql/0090-mod-http-session-table.sql")} + + {:name "0091-mod-team-project-profile-rel-table" + :fn (mg/resource "app/migrations/sql/0091-mod-team-project-profile-rel-table.sql")} + + {:name "0092-mod-team-invitation-table" + :fn (mg/resource "app/migrations/sql/0092-mod-team-invitation-table.sql")} + + {:name "0093-del-file-share-tokens-table" + :fn (mg/resource "app/migrations/sql/0093-del-file-share-tokens-table.sql")} + + {:name "0094-del-profile-attr-table" + :fn (mg/resource "app/migrations/sql/0094-del-profile-attr-table.sql")} + + {:name "0095-del-storage-data-table" + :fn (mg/resource "app/migrations/sql/0095-del-storage-data-table.sql")} + + {:name "0096-del-storage-pending-table" + :fn (mg/resource "app/migrations/sql/0096-del-storage-pending-table.sql")} + + {:name "0097-mod-profile-table" + :fn (mg/resource "app/migrations/sql/0097-mod-profile-table.sql")} + ]) diff --git a/backend/src/app/migrations/sql/0088-mod-team-profile-rel-table.sql b/backend/src/app/migrations/sql/0088-mod-team-profile-rel-table.sql new file mode 100644 index 000000000..de2215e77 --- /dev/null +++ b/backend/src/app/migrations/sql/0088-mod-team-profile-rel-table.sql @@ -0,0 +1,3 @@ +ALTER TABLE team_profile_rel DROP CONSTRAINT team_profile_rel_pkey; +ALTER TABLE team_profile_rel ADD COLUMN id uuid DEFAULT uuid_generate_v4() PRIMARY KEY; +ALTER TABLE team_profile_rel ADD CONSTRAINT team_profile_rel_unique UNIQUE (team_id, profile_id); diff --git a/backend/src/app/migrations/sql/0089-mod-project-profile-rel-table.sql b/backend/src/app/migrations/sql/0089-mod-project-profile-rel-table.sql new file mode 100644 index 000000000..9df9fa0dc --- /dev/null +++ b/backend/src/app/migrations/sql/0089-mod-project-profile-rel-table.sql @@ -0,0 +1,3 @@ +ALTER TABLE project_profile_rel DROP CONSTRAINT project_profile_rel_pkey; +ALTER TABLE project_profile_rel ADD COLUMN id uuid DEFAULT uuid_generate_v4() PRIMARY KEY; +ALTER TABLE project_profile_rel ADD CONSTRAINT project_profile_rel_unique UNIQUE (project_id, profile_id); diff --git a/backend/src/app/migrations/sql/0090-mod-http-session-table.sql b/backend/src/app/migrations/sql/0090-mod-http-session-table.sql new file mode 100644 index 000000000..11f496298 --- /dev/null +++ b/backend/src/app/migrations/sql/0090-mod-http-session-table.sql @@ -0,0 +1,2 @@ +ALTER TABLE http_session DROP CONSTRAINT http_session_pkey; +ALTER TABLE http_session ADD CONSTRAINT http_session_pkey PRIMARY KEY (id); diff --git a/backend/src/app/migrations/sql/0091-mod-team-project-profile-rel-table.sql b/backend/src/app/migrations/sql/0091-mod-team-project-profile-rel-table.sql new file mode 100644 index 000000000..9b59aaf2a --- /dev/null +++ b/backend/src/app/migrations/sql/0091-mod-team-project-profile-rel-table.sql @@ -0,0 +1,3 @@ +ALTER TABLE team_project_profile_rel DROP CONSTRAINT team_project_profile_rel_pkey; +ALTER TABLE team_project_profile_rel ADD COLUMN id uuid DEFAULT uuid_generate_v4() PRIMARY KEY; +ALTER TABLE team_project_profile_rel ADD CONSTRAINT team_project_profile_rel_unique UNIQUE (team_id, project_id, profile_id); diff --git a/backend/src/app/migrations/sql/0092-mod-team-invitation-table.sql b/backend/src/app/migrations/sql/0092-mod-team-invitation-table.sql new file mode 100644 index 000000000..ee59ddbe0 --- /dev/null +++ b/backend/src/app/migrations/sql/0092-mod-team-invitation-table.sql @@ -0,0 +1,3 @@ +ALTER TABLE team_invitation DROP CONSTRAINT team_invitation_pkey; +ALTER TABLE team_invitation ADD COLUMN id uuid DEFAULT uuid_generate_v4() PRIMARY KEY; +ALTER TABLE team_invitation ADD CONSTRAINT team_invitation_unique UNIQUE (team_id, email_to); diff --git a/backend/src/app/migrations/sql/0093-del-file-share-tokens-table.sql b/backend/src/app/migrations/sql/0093-del-file-share-tokens-table.sql new file mode 100644 index 000000000..21261ab79 --- /dev/null +++ b/backend/src/app/migrations/sql/0093-del-file-share-tokens-table.sql @@ -0,0 +1 @@ +DROP TABLE file_share_token; diff --git a/backend/src/app/migrations/sql/0094-del-profile-attr-table.sql b/backend/src/app/migrations/sql/0094-del-profile-attr-table.sql new file mode 100644 index 000000000..0793f48ea --- /dev/null +++ b/backend/src/app/migrations/sql/0094-del-profile-attr-table.sql @@ -0,0 +1 @@ +DROP TABLE profile_attr; diff --git a/backend/src/app/migrations/sql/0095-del-storage-data-table.sql b/backend/src/app/migrations/sql/0095-del-storage-data-table.sql new file mode 100644 index 000000000..e35d40f58 --- /dev/null +++ b/backend/src/app/migrations/sql/0095-del-storage-data-table.sql @@ -0,0 +1 @@ +DROP TABLE storage_data; diff --git a/backend/src/app/migrations/sql/0096-del-storage-pending-table.sql b/backend/src/app/migrations/sql/0096-del-storage-pending-table.sql new file mode 100644 index 000000000..0ebb2a5c3 --- /dev/null +++ b/backend/src/app/migrations/sql/0096-del-storage-pending-table.sql @@ -0,0 +1 @@ +DROP TABLE storage_pending; diff --git a/backend/src/app/migrations/sql/0097-mod-profile-table.sql b/backend/src/app/migrations/sql/0097-mod-profile-table.sql new file mode 100644 index 000000000..9e3ab6212 --- /dev/null +++ b/backend/src/app/migrations/sql/0097-mod-profile-table.sql @@ -0,0 +1,2 @@ +ALTER TABLE profile + ADD COLUMN is_admin boolean DEFAULT false; diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 463dc3069..7ba271128 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -35,6 +35,8 @@ [yetti.request :as yrq] [yetti.response :as yrs])) +(s/def ::profile-id ::us/uuid) + (defn- default-handler [_] (p/rejected (ex/error :type :not-found))) @@ -72,8 +74,11 @@ (let [type (keyword (:type params)) data (into {::http/request request} params) data (if profile-id - (assoc data :profile-id profile-id ::session-id session-id) - (dissoc data :profile-id)) + (assoc data + :profile-id profile-id + ::profile-id profile-id + ::session-id session-id) + (dissoc data :profile-id ::profile-id)) method (get methods type default-handler)] (-> (method data) @@ -90,8 +95,11 @@ (let [type (keyword (:type params)) data (into {::http/request request} params) data (if profile-id - (assoc data :profile-id profile-id ::session-id session-id) - (dissoc data :profile-id)) + (assoc data + :profile-id profile-id + ::profile-id profile-id + ::session-id session-id) + (dissoc data :profile-id ::profile-id)) method (get methods type default-handler)] (-> (method data) @@ -109,9 +117,8 @@ etag (yrq/get-header request "if-none-match") data (into {::http/request request ::cond/key etag} params) data (if profile-id - (assoc data :profile-id profile-id ::session-id session-id) - (dissoc data :profile-id)) - + (assoc data ::profile-id profile-id ::session-id session-id) + (dissoc data ::profile-id)) method (get methods cmd default-handler)] (binding [cond/*enabled* true] (-> (method data) @@ -152,9 +159,12 @@ (letfn [(handle-audit [params result] (let [resultm (meta result) request (::http/request params) + profile-id (or (::audit/profile-id resultm) (:profile-id result) - (:profile-id params) + (if (= (::type cfg) "command") + (::profile-id params) + (:profile-id params)) uuid/zero) props (-> (or (::audit/replace-props resultm) @@ -209,21 +219,24 @@ (wrap-audit cfg $ mdata)) spec (or (::sv/spec mdata) (s/spec any?)) - auth? (:auth mdata true)] + 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. - - (p/do! - (if (and auth? (not (uuid? (:profile-id params)))) - (ex/raise :type :authentication - :code :authentication-required - :hint "authentication required for this endpoint") - (let [params (us/conform spec params)] - (f cfg params))))) + (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))) (defn- process-method @@ -262,6 +275,7 @@ (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.profile 'app.rpc.commands.management 'app.rpc.commands.verify-token 'app.rpc.commands.search diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index df692d7f4..44efcd7f4 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -15,6 +15,7 @@ [app.db :as db] [app.http :as-alias http] [app.loggers.audit :as audit] + [app.rpc :as-alias rpc] [app.rpc.climit :as-alias climit] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] @@ -41,7 +42,7 @@ :profile-id :ip-addr :props :context]) (defn- handle-events - [{:keys [::db/pool]} {:keys [profile-id events ::http/request] :as params}] + [{:keys [::db/pool]} {:keys [::rpc/profile-id events ::http/request] :as params}] (let [ip-addr (audit/parse-client-ip request) xform (comp (map #(assoc % :profile-id profile-id)) @@ -53,7 +54,6 @@ (when (seq events) (db/insert-multi! pool :audit-log event-columns events)))) -(s/def ::profile-id ::us/uuid) (s/def ::name ::us/string) (s/def ::type ::us/string) (s/def ::props (s/map-of ::us/keyword any?)) @@ -67,7 +67,8 @@ (s/def ::events (s/every ::event)) (s/def ::push-audit-events - (s/keys :req-un [::events ::profile-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::events])) (sv/defmethod ::push-audit-events {::climit/queue :push-audit-events diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 9ad8cbf1e..894e98179 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -6,6 +6,7 @@ (ns app.rpc.commands.auth (:require + [app.auth :as auth] [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] @@ -15,6 +16,8 @@ [app.emails :as eml] [app.http.session :as session] [app.loggers.audit :as audit] + [app.main :as-alias main] + [app.rpc :as-alias rpc] [app.rpc.climit :as climit] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] @@ -23,7 +26,6 @@ [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] - [buddy.hashers :as hashers] [clojure.spec.alpha :as s] [cuerdas.core :as str])) @@ -31,7 +33,6 @@ (s/def ::fullname ::us/not-empty-string) (s/def ::lang ::us/string) (s/def ::path ::us/string) -(s/def ::profile-id ::us/uuid) (s/def ::password ::us/not-empty-string) (s/def ::old-password ::us/not-empty-string) (s/def ::theme ::us/string) @@ -40,22 +41,6 @@ ;; ---- HELPERS -(defn derive-password - [password] - (hashers/derive password - {:alg :argon2id - :memory 16384 - :iterations 20 - :parallelism 2})) - -(defn verify-password - [attempt password] - (try - (hashers/verify attempt password) - (catch Exception _e - {:update false - :valid false}))) - (defn email-domain-in-whitelist? "Returns true if email's domain is in the given whitelist or if given whitelist is an empty string." @@ -84,9 +69,10 @@ ;; ---- COMMAND: login with password (defn login-with-password - [{:keys [pool session sprops] :as cfg} {:keys [email password] :as params}] + [{:keys [::db/pool session] :as cfg} {:keys [email password scope] :as params}] - (when-not (contains? cf/flags :login) + (when-not (or (contains? cf/flags :login) + (contains? cf/flags :login-with-password)) (ex/raise :type :restriction :code :login-disabled :hint "login is disabled in this instance")) @@ -96,7 +82,7 @@ (ex/raise :type :validation :code :account-without-password :hint "the current account does not have password")) - (:valid (verify-password password (:password profile)))) + (:valid (auth/verify-password password (:password profile)))) (validate-profile [profile] (when-not profile @@ -126,27 +112,37 @@ (profile/decode-profile-row)) invitation (when-let [token (:invitation-token params)] - (tokens/verify sprops {:token token :iss :team-invitation})) + (tokens/verify (::main/props cfg) {:token token :iss :team-invitation})) ;; If invitation member-id does not matches the profile-id, we just proceed to ignore the ;; invitation because invitations matches exactly; and user can't login with other email and ;; accept invitation with other email response (if (and (some? invitation) (= (:id profile) (:member-id invitation))) {:invitation-token (:invitation-token params)} - profile)] + (update profile :is-admin (fn [admin?] + (or admin? + (let [admins (cf/get :admins)] + (contains? admins (:email profile)))))))] + + (when (and (nil? (:default-team-id profile)) + (not= scope "admin")) + (ex/raise :type :restriction + :code :admin-only-profile + :hint "can't login with admin-only profile")) (-> response (rph/with-transform (session/create-fn session (: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])) + :opt-un [::invitation-token ::scope])) (sv/defmethod ::login-with-password "Performs authentication using penpot password." - {:auth false + {::rpc/auth false ::climit/queue :auth ::doc/added "1.15"} [cfg params] @@ -155,11 +151,11 @@ ;; ---- COMMAND: Logout (s/def ::logout - (s/keys :opt-un [::profile-id])) + (s/keys :opt [::rpc/profile-id])) (sv/defmethod ::logout "Clears the authentication cookie and logout the current session." - {:auth false + {::rpc/auth false ::doc/added "1.15"} [{:keys [session] :as cfg} _] (rph/with-transform {} (session/delete-fn session))) @@ -167,13 +163,13 @@ ;; ---- COMMAND: Recover Profile (defn recover-profile - [{:keys [pool sprops] :as cfg} {:keys [token password]}] + [{:keys [::db/pool] :as cfg} {:keys [token password]}] (letfn [(validate-token [token] - (let [tdata (tokens/verify sprops {:token token :iss :password-recovery})] + (let [tdata (tokens/verify (::main/props cfg) {:token token :iss :password-recovery})] (:profile-id tdata))) (update-password [conn profile-id] - (let [pwd (derive-password password)] + (let [pwd (auth/derive-password password)] (db/update! conn :profile {:password pwd} {:id profile-id})))] (db/with-atomic [conn pool] @@ -186,7 +182,7 @@ (s/keys :req-un [::token ::password])) (sv/defmethod ::recover-profile - {:auth false + {::rpc/auth false ::climit/queue :auth ::doc/added "1.15"} [cfg params] @@ -195,13 +191,13 @@ ;; ---- COMMAND: Prepare Register (defn validate-register-attempt! - [{:keys [pool sprops]} params] + [{:keys [::db/pool] :as cfg} params] (when-not (contains? cf/flags :registration) (if-not (contains? params :invitation-token) (ex/raise :type :restriction :code :registration-disabled) - (let [invitation (tokens/verify sprops {:token (:invitation-token params) :iss :team-invitation})] + (let [invitation (tokens/verify (::main/props cfg) {:token (:invitation-token params) :iss :team-invitation})] (when-not (= (:email params) (:member-email invitation)) (ex/raise :type :restriction :code :email-does-not-match-invitation @@ -235,7 +231,7 @@ (pos? (compare elapsed register-retry-threshold)))) (defn prepare-register - [{:keys [pool sprops] :as cfg} params] + [{:keys [::db/pool] :as cfg} params] (validate-register-attempt! cfg params) @@ -264,7 +260,7 @@ params (d/without-nils params) - token (tokens/generate sprops params)] + token (tokens/generate (::main/props cfg) params)] (with-meta {:token token} {::audit/profile-id uuid/zero}))) @@ -273,7 +269,7 @@ :opt-un [::invitation-token])) (sv/defmethod ::prepare-register-profile - {:auth false + {::rpc/auth false ::doc/added "1.15"} [cfg params] (prepare-register cfg params)) @@ -293,7 +289,7 @@ (db/tjson)) password (if-let [password (:password params)] - (derive-password password) + (auth/derive-password password) "!") locale (:locale params) @@ -339,15 +335,15 @@ (assoc :default-project-id (:default-project-id team))))) (defn send-email-verification! - [conn sprops profile] - (let [vtoken (tokens/generate sprops + [conn props profile] + (let [vtoken (tokens/generate props {:iss :verify-email :exp (dt/in-future "72h") :profile-id (:id profile) :email (:email profile)}) ;; NOTE: this token is mainly used for possible complains ;; identification on the sns webhook - ptoken (tokens/generate sprops + ptoken (tokens/generate props {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})] @@ -360,8 +356,8 @@ :extra-data ptoken}))) (defn register-profile - [{:keys [conn sprops session] :as cfg} {:keys [token] :as params}] - (let [claims (tokens/verify sprops {:token token :iss :prepared-register}) + [{:keys [conn session] :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) @@ -377,7 +373,7 @@ (create-profile-relations conn) (profile/decode-profile-row))) invitation (when-let [token (:invitation-token params)] - (tokens/verify sprops {:token token :iss :team-invitation}))] + (tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))] ;; If profile is filled in claims, means it tries to register ;; again, so we proceed to update the modified-at attr @@ -399,7 +395,7 @@ ;; email. (and (some? invitation) (= (:email profile) (:member-email invitation))) (let [claims (assoc invitation :member-id (:id profile)) - token (tokens/generate sprops claims) + token (tokens/generate (::main/props cfg) claims) resp {:invitation-token token}] (-> resp (rph/with-transform (session/create-fn session (:id profile))) @@ -426,7 +422,7 @@ ;; In all other cases, send a verification email. :else (do - (send-email-verification! conn sprops profile) + (send-email-verification! conn (::main/props cfg) profile) (rph/with-meta profile {::audit/replace-props (audit/profile->props profile) ::audit/profile-id (:id profile)}))))) @@ -435,10 +431,10 @@ (s/keys :req-un [::token ::fullname])) (sv/defmethod ::register-profile - {:auth false + {::rpc/auth false ::climit/queue :auth ::doc/added "1.15"} - [{:keys [pool] :as cfg} params] + [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] (-> (assoc cfg :conn conn) (register-profile params)))) @@ -446,16 +442,16 @@ ;; ---- COMMAND: Request Profile Recovery (defn request-profile-recovery - [{:keys [pool sprops] :as cfg} {:keys [email] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [email] :as params}] (letfn [(create-recovery-token [{:keys [id] :as profile}] - (let [token (tokens/generate sprops + (let [token (tokens/generate (::main/props cfg) {:iss :password-recovery :exp (dt/in-future "15m") :profile-id id})] (assoc profile :token token))) (send-email-notification [conn profile] - (let [ptoken (tokens/generate sprops + (let [ptoken (tokens/generate (::main/props cfg) {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})] @@ -493,7 +489,7 @@ (s/keys :req-un [::email])) (sv/defmethod ::request-profile-recovery - {:auth false + {::rpc/auth false ::doc/added "1.15"} [cfg params] (request-profile-recovery cfg params)) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index c9cf634b4..bd7eb1b61 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -15,10 +15,13 @@ [app.common.uuid :as uuid] [app.config :as cf] [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.commands.files :as files] [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] @@ -291,7 +294,7 @@ (defn- retrieve-file [pool file-id] - (with-open [conn (db/open pool)] + (with-open [^AutoCloseable conn (db/open pool)] (binding [pmap/*load-fn* (partial files/load-pointer conn file-id)] (some-> (db/get* conn :file {:id file-id}) (files/decode-row) @@ -864,18 +867,17 @@ ;; --- Command: export-binfile (s/def ::file-id ::us/uuid) -(s/def ::profile-id ::us/uuid) (s/def ::include-libraries? ::us/boolean) (s/def ::embed-assets? ::us/boolean) (s/def ::export-binfile - (s/keys :req-un [::profile-id ::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 [profile-id file-id include-libraries? embed-assets?] :as params}] + [{:keys [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] @@ -890,16 +892,18 @@ (s/def ::file ::media/upload) (s/def ::import-binfile - (s/keys :req-un [::profile-id ::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." {::doc/added "1.15" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [profile-id project-id file] :as params}] + [{:keys [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) - (import! (assoc cfg - ::input (:path file) - ::project-id project-id - ::ignore-index-errors? true)))) + (let [ids (import! (assoc cfg + ::input (:path file) + ::project-id project-id + ::ignore-index-errors? true))] + (rph/with-meta ids + {::audit/props {:file nil :file-ids ids}})))) diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index 9e45f417d..2d0d6734f 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -12,6 +12,7 @@ [app.db :as db] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] + [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] @@ -41,7 +42,7 @@ (s/def ::share-id (s/nilable ::us/uuid)) (s/def ::get-comment-threads - (s/and (s/keys :req-un [::profile-id] + (s/and (s/keys :req [::rpc/profile-id] :opt-un [::file-id ::share-id ::team-id]) #(or (:file-id %) (:team-id %)))) @@ -74,7 +75,7 @@ window w as (partition by c.thread_id order by c.created_at asc)") (defn retrieve-comment-threads - [conn {:keys [profile-id file-id share-id]}] + [conn {:keys [::rpc/profile-id file-id share-id]}] (files/check-comment-permissions! conn profile-id file-id share-id) (->> (db/exec! conn [sql:comment-threads profile-id file-id]) (into [] (map decode-row)))) @@ -85,11 +86,12 @@ (s/def ::team-id ::us/uuid) (s/def ::get-unread-comment-threads - (s/keys :req-un [::profile-id ::team-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id])) (sv/defmethod ::get-unread-comment-threads {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] + [{:keys [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) (retrieve-unread-comment-threads conn params))) @@ -122,7 +124,7 @@ "select * from threads where count_unread_comments > 0")) (defn retrieve-unread-comment-threads - [conn {:keys [profile-id team-id]}] + [conn {:keys [::rpc/profile-id team-id]}] (->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id]) (into [] (map decode-row)))) @@ -132,12 +134,13 @@ (s/def ::id ::us/uuid) (s/def ::share-id (s/nilable ::us/uuid)) (s/def ::get-comment-thread - (s/keys :req-un [::profile-id ::file-id ::id] + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::id] :opt-un [::share-id])) (sv/defmethod ::get-comment-thread {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id id share-id] :as params}] + [{:keys [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 ")" @@ -146,7 +149,7 @@ (decode-row))))) (defn get-comment-thread - [conn {:keys [profile-id file-id id] :as params}] + [conn {:keys [::rpc/profile-id file-id id] :as params}] (let [sql (str "with threads as (" sql:comment-threads ")" "select * from threads where id = ?")] (-> (db/exec-one! conn [sql profile-id file-id id]) @@ -160,12 +163,13 @@ (s/def ::share-id (s/nilable ::us/uuid)) (s/def ::thread-id ::us/uuid) (s/def ::get-comments - (s/keys :req-un [::profile-id ::thread-id] + (s/keys :req [::rpc/profile-id] + :req-un [::thread-id] :opt-un [::share-id])) (sv/defmethod ::get-comments {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id thread-id share-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}] (with-open [conn (db/open pool)] (let [thread (db/get-by-id conn :comment-thread thread-id)] (files/check-comment-permissions! conn profile-id (:file-id thread) share-id)) @@ -191,7 +195,8 @@ (s/def ::share-id (s/nilable ::us/uuid)) (s/def ::get-profiles-for-file-comments - (s/keys :req-un [::profile-id ::file-id] + (s/keys :req [::rpc/profile-id] + :req-un [::file-id] :opt-un [::share-id])) (sv/defmethod ::get-profiles-for-file-comments @@ -199,7 +204,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 [profile-id file-id share-id]}] + [{:keys [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))) @@ -240,19 +245,19 @@ (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) (s/def ::share-id (s/nilable ::us/uuid)) -(s/def ::profile-id ::us/uuid) (s/def ::position ::gpt/point) (s/def ::content ::us/string) (s/def ::frame-id ::us/uuid) (s/def ::create-comment-thread - (s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id ::frame-id] + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::position ::content ::page-id ::frame-id] :opt-un [::share-id])) (sv/defmethod ::create-comment-thread {::doc/added "1.15" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}] (db/with-atomic [conn pool] (files/check-comment-permissions! conn profile-id file-id share-id) @@ -268,7 +273,7 @@ (:next-seqn res))) (defn create-comment-thread - [conn {:keys [profile-id file-id page-id position content frame-id] :as params}] + [conn {:keys [::rpc/profile-id file-id page-id position content frame-id] :as params}] (let [seqn (retrieve-next-seqn conn file-id) now (dt/now) pname (retrieve-page-name conn params) @@ -316,12 +321,13 @@ (s/def ::share-id (s/nilable ::us/uuid)) (s/def ::update-comment-thread-status - (s/keys :req-un [::profile-id ::id] + (s/keys :req [::rpc/profile-id] + :req-un [::id] :opt-un [::share-id])) (sv/defmethod ::update-comment-thread-status {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id share-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}] (db/with-atomic [conn pool] (let [cthr (db/get-by-id conn :comment-thread id {:for-update true})] (when-not cthr @@ -346,12 +352,13 @@ (s/def ::is-resolved ::us/boolean) (s/def ::update-comment-thread - (s/keys :req-un [::profile-id ::id ::is-resolved] + (s/keys :req [::rpc/profile-id] + :req-un [::id ::is-resolved] :opt-un [::share-id])) (sv/defmethod ::update-comment-thread {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id is-resolved share-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}] (db/with-atomic [conn pool] (let [thread (db/get-by-id conn :comment-thread id {:for-update true})] (when-not thread @@ -370,7 +377,8 @@ (declare create-comment) (s/def ::create-comment - (s/keys :req-un [::profile-id ::thread-id ::content] + (s/keys :req [::rpc/profile-id] + :req-un [::thread-id ::content] :opt-un [::share-id])) (sv/defmethod ::create-comment @@ -381,7 +389,7 @@ (create-comment conn params))) (defn create-comment - [conn {:keys [profile-id thread-id content share-id] :as params}] + [conn {:keys [::rpc/profile-id thread-id content share-id] :as params}] (let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true}) (decode-row)) pname (retrieve-page-name conn thread)] @@ -437,7 +445,8 @@ (declare update-comment) (s/def ::update-comment - (s/keys :req-un [::profile-id ::id ::content] + (s/keys :req [::rpc/profile-id] + :req-un [::id ::content] :opt-un [::share-id])) (sv/defmethod ::update-comment @@ -447,7 +456,7 @@ (update-comment conn params))) (defn update-comment - [conn {:keys [profile-id id content share-id] :as params}] + [conn {:keys [::rpc/profile-id id content share-id] :as params}] (let [comment (db/get-by-id conn :comment id {:for-update true}) _ (when-not comment (ex/raise :type :not-found)) thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true}) @@ -476,11 +485,12 @@ ;; --- COMMAND: Delete Comment Thread (s/def ::delete-comment-thread - (s/keys :req-un [::profile-id ::id])) + (s/keys :req [::rpc/profile-id] + :req-un [::id])) (sv/defmethod ::delete-comment-thread {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (let [thread (db/get-by-id conn :comment-thread id {:for-update true})] (when-not (= (:owner-id thread) profile-id) @@ -493,11 +503,12 @@ ;; --- COMMAND: Delete comment (s/def ::delete-comment - (s/keys :req-un [::profile-id ::id])) + (s/keys :req [::rpc/profile-id] + :req-un [::id])) (sv/defmethod ::delete-comment {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (let [comment (db/get-by-id conn :comment id {:for-update true})] (when-not (= (:owner-id comment) profile-id) @@ -509,12 +520,13 @@ ;; --- COMMAND: Update comment thread position (s/def ::update-comment-thread-position - (s/keys :req-un [::profile-id ::id ::position ::frame-id] + (s/keys :req [::rpc/profile-id] + :req-un [::id ::position ::frame-id] :opt-un [::share-id])) (sv/defmethod ::update-comment-thread-position {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id position frame-id share-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}] (db/with-atomic [conn pool] (let [thread (db/get-by-id conn :comment-thread id {:for-update true})] (files/check-comment-permissions! conn profile-id (:file-id thread) share-id) @@ -528,12 +540,13 @@ ;; --- COMMAND: Update comment frame (s/def ::update-comment-thread-frame - (s/keys :req-un [::profile-id ::id ::frame-id] + (s/keys :req [::rpc/profile-id] + :req-un [::id ::frame-id] :opt-un [::share-id])) (sv/defmethod ::update-comment-thread-frame {::doc/added "1.15"} - [{:keys [pool] :as cfg} {:keys [profile-id id frame-id share-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}] (db/with-atomic [conn pool] (let [thread (db/get-by-id conn :comment-thread id {:for-update true})] (files/check-comment-permissions! conn profile-id (:file-id thread) share-id) diff --git a/backend/src/app/rpc/commands/demo.clj b/backend/src/app/rpc/commands/demo.clj index aac3c1ded..bcc3d1d6e 100644 --- a/backend/src/app/rpc/commands/demo.clj +++ b/backend/src/app/rpc/commands/demo.clj @@ -12,6 +12,7 @@ [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.doc :as-alias doc] [app.util.services :as sv] @@ -26,7 +27,7 @@ "A command that is responsible of creating a demo purpose profile. It only works if the `demo-users` flag is enabled in the configuration." - {:auth false + {::rpc/auth false ::doc/added "1.15" ::doc/changes ["1.15" "This method is migrated from mutations to commands."]} [{:keys [pool] :as cfg} _] diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 98518feb9..8ec4e9182 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -17,7 +17,9 @@ [app.common.types.shape-tree :as ctt] [app.db :as db] [app.db.sql :as sql] + [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] + [app.rpc :as-alias rpc] [app.rpc.commands.files.thumbnails :as-alias thumbs] [app.rpc.commands.teams :as teams] [app.rpc.cond :as-alias cond] @@ -51,7 +53,6 @@ (s/def ::id ::us/uuid) (s/def ::is-shared ::us/boolean) (s/def ::name ::us/string) -(s/def ::profile-id ::us/uuid) (s/def ::project-id ::us/uuid) (s/def ::search-term ::us/string) (s/def ::team-id ::us/uuid) @@ -256,7 +257,8 @@ (str (dt/format-instant modified-at :iso) "-" revn)) (s/def ::get-file - (s/keys :req-un [::profile-id ::id] + (s/keys :req [::rpc/profile-id] + :req-un [::id] :opt-un [::features])) (sv/defmethod ::get-file @@ -264,7 +266,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 [profile-id id features] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id features] :as params}] (with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id id)] (check-read-permissions! perms) @@ -285,13 +287,14 @@ (s/def ::get-file-fragment (s/keys :req-un [::file-id ::fragment-id] - :opt-un [::share-id ::profile-id])) + :opt [::rpc/profile-id] + :opt-un [::share-id])) (sv/defmethod ::get-file-fragment "Retrieve a file by its ID. Only authenticated users." {::doc/added "1.17" - :auth false} - [{:keys [pool] :as cfg} {:keys [profile-id file-id fragment-id share-id] :as params}] + ::rpc/:auth false} + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] :as params}] (with-open [conn (db/open pool)] (let [perms (get-permissions conn profile-id file-id share-id)] (check-read-permissions! perms) @@ -319,7 +322,7 @@ (d/index-by :object-id :data))))) (s/def ::get-file-object-thumbnails - (s/keys :req-un [::profile-id ::file-id])) + (s/keys :req [::rpc/profile-id] :req-un [::file-id])) (sv/defmethod ::get-file-object-thumbnails "Retrieve a file object thumbnails." @@ -327,7 +330,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 [profile-id file-id] :as params}] + [{:keys [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))) @@ -349,7 +352,7 @@ order by f.modified_at desc") (s/def ::get-project-files - (s/keys :req-un [::profile-id ::project-id])) + (s/keys :req [::rpc/profile-id] :req-un [::project-id])) (defn get-project-files [conn project-id] @@ -358,7 +361,7 @@ (sv/defmethod ::get-project-files "Get all files for the specified project." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id] :as params}] (with-open [conn (db/open pool)] (projects/check-read-permissions! conn profile-id project-id) (get-project-files conn project-id))) @@ -369,15 +372,14 @@ (declare get-has-file-libraries) (s/def ::file-id ::us/uuid) -(s/def ::profile-id ::us/uuid) (s/def ::has-file-libraries - (s/keys :req-un [::profile-id ::file-id])) + (s/keys :req [::rpc/profile-id] :req-un [::file-id])) (sv/defmethod ::has-file-libraries "Checks if the file has libraries. Returns a boolean" {::doc/added "1.15.1"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (with-open [conn (db/open pool)] (check-read-permissions! pool profile-id file-id) (get-has-file-libraries conn params))) @@ -425,7 +427,8 @@ (s/def ::object-id ::us/uuid) (s/def ::get-page (s/and - (s/keys :req-un [::profile-id ::file-id] + (s/keys :req [::rpc/profile-id] + :req-un [::file-id] :opt-un [::page-id ::object-id ::features]) (fn [obj] (if (contains? obj :object-id) @@ -443,7 +446,7 @@ Mainly used for rendering purposes." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + [{:keys [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))) @@ -492,7 +495,7 @@ (into #{} xform (db/exec! conn [sql:team-shared-files team-id])))) (s/def ::get-team-shared-files - (s/keys :req-un [::profile-id ::team-id])) + (s/keys :req [::rpc/profile-id] :req-un [::team-id])) (sv/defmethod ::get-team-shared-files "Get all file (libraries) for the specified team." @@ -541,13 +544,14 @@ (handle-file-features client-features))))))) (s/def ::get-file-libraries - (s/keys :req-un [::profile-id ::file-id] + (s/keys :req [::rpc/profile-id] + :req-un [::file-id] :opt-un [::features])) (sv/defmethod ::get-file-libraries "Get libraries used by the specified file." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id features] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id features] :as params}] (with-open [conn (db/open pool)] (check-read-permissions! conn profile-id file-id) (get-file-libraries conn file-id features))) @@ -568,12 +572,12 @@ (db/exec! conn [sql:library-using-files file-id])) (s/def ::get-library-file-references - (s/keys :req-un [::profile-id ::file-id])) + (s/keys :req [::rpc/profile-id] :req-un [::file-id])) (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 [profile-id file-id] :as params}] + [{:keys [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))) @@ -606,11 +610,12 @@ (db/exec! conn [sql:team-recent-files team-id])) (s/def ::get-team-recent-files - (s/keys :req-un [::profile-id ::team-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id])) (sv/defmethod ::get-team-recent-files {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] + [{:keys [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))) @@ -638,12 +643,13 @@ (s/def ::revn ::us/integer) (s/def ::get-file-thumbnail - (s/keys :req-un [::profile-id ::file-id] + (s/keys :req [::rpc/profile-id] + :req-un [::file-id] :opt-un [::revn])) (sv/defmethod ::get-file-thumbnail {::doc/added "1.17"} - [{:keys [pool]} {:keys [profile-id file-id revn]}] + [{:keys [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) @@ -729,14 +735,15 @@ (update :objects assoc-thumbnails page-id thumbs))))) (s/def ::get-file-data-for-thumbnail - (s/keys :req-un [::profile-id ::file-id] + (s/keys :req [::rpc/profile-id] + :req-un [::file-id] :opt-un [::features])) (sv/defmethod ::get-file-data-for-thumbnail "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 [profile-id file-id features] :as props}] + [{:keys [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) (let [file (get-file conn file-id features)] @@ -753,23 +760,27 @@ (defn rename-file [conn {:keys [id name] :as params}] - (-> (db/update! conn :file - {:name name - :modified-at (dt/now)} - {:id id}) - (select-keys [:id :name :created-at :modified-at]))) + (db/update! conn :file + {:name name + :modified-at (dt/now)} + {:id id})) (s/def ::rename-file - (s/keys :req-un [::profile-id ::name ::id])) + (s/keys :req [::rpc/profile-id] + :req-un [::name ::id])) (sv/defmethod ::rename-file {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) - (rename-file conn params))) - + (let [file (rename-file conn params)] + (rph/with-meta + (select-keys file [:id :name :created-at :modified-at]) + {::audit/props {:project-id (:project-id file) + :created-at (:created-at file) + :modified-at (:modified-at file)}})))) ;; --- MUTATION COMMAND: set-file-shared @@ -779,10 +790,9 @@ (defn set-file-shared [conn {:keys [id is-shared] :as params}] - (-> (db/update! conn :file - {:is-shared is-shared} - {:id id}) - (select-keys [:id :name :is-shared]))) + (db/update! conn :file + {:is-shared is-shared} + {:id id})) (defn absorb-library "Find all files using a shared library, and absorb all library assets @@ -805,19 +815,25 @@ {:id id}))))))))) (s/def ::set-file-shared - (s/keys :req-un [::profile-id ::id ::is-shared])) + (s/keys :req [::rpc/profile-id] + :req-un [::id ::is-shared])) (sv/defmethod ::set-file-shared {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [id profile-id is-shared] :as params}] + [{:keys [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 (absorb-library conn params) (unlink-files conn params)) - (set-file-shared conn params))) + (let [file (set-file-shared conn params)] + (rph/with-meta + (select-keys file [:id :name :is-shared]) + {::audit/props {:name (:name file) + :project-id (:project-id file) + :is-shared (:is-shared file)}})))) ;; --- MUTATION COMMAND: delete-file @@ -825,20 +841,26 @@ [conn {:keys [id] :as params}] (db/update! conn :file {:deleted-at (dt/now)} - {:id id}) - nil) + {:id id})) (s/def ::delete-file - (s/keys :req-un [::id ::profile-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::id])) (sv/defmethod ::delete-file {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] + [{:keys [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) - (mark-file-deleted conn params))) + (let [file (mark-file-deleted conn params)] + + (rph/with-meta (rph/wrap) + {::audit/props {:project-id (:project-id file) + :name (:name file) + :created-at (:created-at file) + :modified-at (:modified-at file)}})))) ;; --- MUTATION COMMAND: link-file-to-library @@ -852,12 +874,13 @@ (db/exec-one! conn [sql:link-file-to-library file-id library-id])) (s/def ::link-file-to-library - (s/keys :req-un [::profile-id ::file-id ::library-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::library-id])) (sv/defmethod ::link-file-to-library {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}] + [{:keys [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 @@ -876,12 +899,13 @@ :library-file-id library-id})) (s/def ::unlink-file-from-library - (s/keys :req-un [::profile-id ::file-id ::library-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::library-id])) (sv/defmethod ::unlink-file-from-library {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + [{:keys [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))) @@ -897,14 +921,15 @@ :library-file-id library-id})) (s/def ::update-file-library-sync-status - (s/keys :req-un [::profile-id ::file-id ::library-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::library-id])) ;; TODO: improve naming (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 [profile-id file-id] :as params}] + [{:keys [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))) @@ -919,13 +944,14 @@ {:id file-id})) (s/def ::ignore-file-library-sync-status - (s/keys :req-un [::profile-id ::file-id ::date])) + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::date])) ;; TODO: improve naming (sv/defmethod ::ignore-file-library-sync-status "Ignore updates in linked files" {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + [{:keys [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))) @@ -948,11 +974,13 @@ (s/def ::data (s/nilable ::us/string)) (s/def ::thumbs/object-id ::us/string) (s/def ::upsert-file-object-thumbnail - (s/keys :req-un [::profile-id ::file-id ::thumbs/object-id ::data])) + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::thumbs/object-id] + :opt-un [::data])) (sv/defmethod ::upsert-file-object-thumbnail {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + [{:keys [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) @@ -975,13 +1003,14 @@ (s/def ::revn ::us/integer) (s/def ::props map?) (s/def ::upsert-file-thumbnail - (s/keys :req-un [::profile-id ::file-id ::revn ::data ::props])) + (s/keys :req [::rpc/profile-id] + :req-un [::file-id ::revn ::data ::props])) (sv/defmethod ::upsert-file-thumbnail "Creates or updates the file thumbnail. Mainly used for paint the grid thumbnails." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}] + [{:keys [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 d0283abca..4a2b4d641 100644 --- a/backend/src/app/rpc/commands/files/create.clj +++ b/backend/src/app/rpc/commands/files/create.clj @@ -13,6 +13,7 @@ [app.db :as db] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] + [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] [app.rpc.doc :as-alias doc] [app.rpc.permissions :as perms] @@ -68,8 +69,8 @@ (files/decode-row file))) (s/def ::create-file - (s/keys :req-un [::files/profile-id - ::files/name + (s/keys :req [::rpc/profile-id] + :req-un [::files/name ::files/project-id] :opt-un [::files/id ::files/is-shared @@ -78,10 +79,11 @@ (sv/defmethod ::create-file {::doc/added "1.17" ::webhooks/event? true} - [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] + [{: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) - (let [team-id (files/get-team-id conn project-id)] + (let [team-id (files/get-team-id conn project-id) + params (assoc params :profile-id profile-id)] (-> (create-file conn params) (vary-meta assoc ::audit/props {:team-id team-id}))))) diff --git a/backend/src/app/rpc/commands/files/temp.clj b/backend/src/app/rpc/commands/files/temp.clj index 3dbee423d..74468f486 100644 --- a/backend/src/app/rpc/commands/files/temp.clj +++ b/backend/src/app/rpc/commands/files/temp.clj @@ -11,6 +11,7 @@ [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.commands.files.create :as files.create] [app.rpc.commands.files.update :as files.update] @@ -26,8 +27,8 @@ (s/def ::create-page ::us/boolean) (s/def ::create-temp-file - (s/keys :req-un [::files/profile-id - ::files/name + (s/keys :req [::rpc/profile-id] + :req-un [::files/name ::files/project-id] :opt-un [::files/id ::files/is-shared @@ -36,7 +37,7 @@ (sv/defmethod ::create-temp-file {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}] + [{: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 :deleted-at (dt/in-future {:days 1}))))) @@ -44,7 +45,7 @@ ;; --- MUTATION COMMAND: update-temp-file (defn update-temp-file - [conn {:keys [profile-id session-id id revn changes] :as params}] + [conn {:keys [::rpc/profile-id session-id id revn changes] :as params}] (db/insert! conn :file-change {:id (uuid/next) :session-id session-id @@ -95,12 +96,12 @@ nil)) (s/def ::persist-temp-file - (s/keys :req-un [::files/id - ::files/profile-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::files/id])) (sv/defmethod ::persist-temp-file {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] + [{:keys [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 991dcadef..7c788553c 100644 --- a/backend/src/app/rpc/commands/files/update.clj +++ b/backend/src/app/rpc/commands/files/update.clj @@ -20,6 +20,7 @@ [app.loggers.webhooks :as-alias webhooks] [app.metrics :as mtx] [app.msgbus :as mbus] + [app.rpc :as-alias rpc] [app.rpc.climit :as-alias climit] [app.rpc.commands.files :as files] [app.rpc.doc :as-alias doc] @@ -52,7 +53,8 @@ (s/def ::revn ::us/integer) (s/def ::update-file (s/and - (s/keys :req-un [::files/id ::files/profile-id ::session-id ::revn] + (s/keys :req [::rpc/profile-id] + :req-un [::files/id ::session-id ::revn] :opt-un [::changes ::changes-with-metadata ::features]) (fn [o] (or (contains? o :changes) @@ -130,19 +132,20 @@ ::webhooks/batch-timeout (dt/duration "2m") ::webhooks/batch-key :id ::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] + [{:keys [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) (let [cfg (assoc cfg :conn conn) + params (assoc params :profile-id profile-id) tpoint (dt/tpoint)] (-> (update-file cfg params) (rph/with-defer #(let [elapsed (tpoint)] (l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))) (defn update-file - [{:keys [conn metrics] :as cfg} {:keys [id profile-id changes changes-with-metadata] :as params}] + [{:keys [conn 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)) @@ -184,7 +187,7 @@ :team-id (:team-id file)})))))) (defn- update-file* - [{:keys [conn] :as cfg} {:keys [file changes session-id profile-id] :as params}] + [{:keys [conn] :as cfg} {:keys [profile-id file changes session-id] :as params}] (when (> (:revn params) (:revn file)) (ex/raise :type :validation diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index ad2fc51b1..f3f882ef5 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -14,6 +14,7 @@ [app.common.uuid :as uuid] [app.db :as db] [app.loggers.webhooks :as-alias webhooks] + [app.rpc :as-alias rpc] [app.rpc.commands.binfile :as binfile] [app.rpc.commands.files :as files] [app.rpc.commands.teams :as teams :refer [create-project-role create-project]] @@ -31,14 +32,14 @@ (declare duplicate-file) (s/def ::id ::us/uuid) -(s/def ::profile-id ::us/uuid) (s/def ::project-id ::us/uuid) (s/def ::file-id ::us/uuid) (s/def ::team-id ::us/uuid) (s/def ::name ::us/string) (s/def ::duplicate-file - (s/keys :req-un [::profile-id ::file-id] + (s/keys :req [::rpc/profile-id] + :req-un [::file-id] :opt-un [::name])) (sv/defmethod ::duplicate-file @@ -47,7 +48,7 @@ ::webhooks/event? true} [{:keys [pool] :as cfg} params] (db/with-atomic [conn pool] - (duplicate-file conn params))) + (duplicate-file conn (assoc params :profile-id (::rpc/profile-id params))))) (defn- remap-id [item index key] @@ -212,7 +213,8 @@ (declare duplicate-project) (s/def ::duplicate-project - (s/keys :req-un [::profile-id ::project-id] + (s/keys :req [::rpc/profile-id] + :req-un [::project-id] :opt-un [::name])) (sv/defmethod ::duplicate-project @@ -221,7 +223,7 @@ ::webhooks/event? true} [{:keys [pool] :as cfg} params] (db/with-atomic [conn pool] - (duplicate-project conn params))) + (duplicate-project conn (assoc params :profile-id (::rpc/profile-id params))))) (defn duplicate-project [conn {:keys [profile-id project-id name] :as params}] @@ -249,9 +251,7 @@ ;; create the duplicated project and assign the current profile as ;; a project owner (create-project conn project) - (create-project-role conn {:project-id (:id project) - :profile-id profile-id - :role :owner}) + (create-project-role conn profile-id (:id project) :owner) ;; duplicate all files (let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} files) @@ -322,7 +322,8 @@ (s/def ::ids (s/every ::us/uuid :kind set?)) (s/def ::move-files - (s/keys :req-un [::profile-id ::ids ::project-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::ids ::project-id])) (sv/defmethod ::move-files "Move a set of files from one project to other." @@ -330,7 +331,7 @@ ::webhooks/event? true} [{:keys [pool] :as cfg} params] (db/with-atomic [conn pool] - (move-files conn params))) + (move-files conn (assoc params :profile-id (::rpc/profile-id params))))) ;; --- COMMAND: Move project @@ -362,7 +363,8 @@ (s/def ::move-project - (s/keys :req-un [::profile-id ::team-id ::project-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::project-id])) (sv/defmethod ::move-project "Move projects between teams." @@ -370,7 +372,7 @@ ::webhooks/event? true} [{:keys [pool] :as cfg} params] (db/with-atomic [conn pool] - (move-project conn params))) + (move-project conn (assoc params :profile-id (::rpc/profile-id params))))) ;; --- COMMAND: Clone Template @@ -378,7 +380,8 @@ (s/def ::template-id ::us/not-empty-string) (s/def ::clone-template - (s/keys :req-un [::profile-id ::project-id ::template-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::project-id ::template-id])) (sv/defmethod ::clone-template "Clone into the specified project the template by its id." @@ -387,7 +390,7 @@ [{:keys [pool] :as cfg} params] (db/with-atomic [conn pool] (-> (assoc cfg :conn conn) - (clone-template params)))) + (clone-template (assoc params :profile-id (::rpc/profile-id params)))))) (defn- clone-template [{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}] diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj new file mode 100644 index 000000000..876b609df --- /dev/null +++ b/backend/src/app/rpc/commands/profile.clj @@ -0,0 +1,75 @@ +;; 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.exceptions :as ex] + [app.common.spec :as us] + [app.config :as cf] + [app.db :as db] + [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] + [app.rpc.doc :as-alias doc] + [app.util.services :as sv] + [clojure.spec.alpha :as s])) + +;; --- MUTATION: Set profile password + +(declare update-profile-password!) + +(s/def ::profile-id ::us/uuid) +(s/def ::password ::us/not-empty-string) + +(s/def ::get-derived-password + (s/keys :req [::rpc/profile-id] + :req-un [::password])) + +(sv/defmethod ::get-derived-password + "Get derived password, only ADMINS allowed to call this RPC + methods. Designed for administration pannel integration." + {::climit/queue :auth + ::climit/key-fn ::rpc/profile-id + ::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [password] :as params}] + (db/with-atomic [conn pool] + (let [admins (cf/get :admins) + profile (db/get-by-id conn :profile (::rpc/profile-id params))] + + (if (or (:is-admin profile) + (contains? admins (:email profile))) + {:password (auth/derive-password password)} + (ex/raise :type :authentication + :code :only-admins-allowed + :hint "only admins allowed to call this RPC method"))))) + +;; --- MUTATION: Check profile password + +(s/def ::attempt ::us/not-empty-string) +(s/def ::check-profile-password + (s/keys :req [::rpc/profile-id] + :req-un [::profile-id ::password])) + +(sv/defmethod ::check-profile-password + "Check profile password, only ADMINS allowed to call this RPC + methods. Designed for administration pannel integration." + {::climit/queue :auth + ::climit/key-fn ::rpc/profile-id + ::doc/added "1.18"} + [{:keys [::db/pool]} {:keys [profile-id password] :as params}] + (db/with-atomic [conn pool] + (let [admins (cf/get :admins) + profile (db/get-by-id pool :profile (::rpc/profile-id params))] + + (if (or (:is-admin profile) + (contains? admins (:email profile))) + (let [profile (if (not= (::rpc/profile-id params) profile-id) + (db/get-by-id conn :profile profile-id) + profile)] + (auth/verify-password password (:password profile))) + (ex/raise :type :authentication + :code :only-admins-allowed + :hint "only admins allowed to call this RPC method"))))) diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj index 23fd19bef..92534fafc 100644 --- a/backend/src/app/rpc/commands/search.clj +++ b/backend/src/app/rpc/commands/search.clj @@ -8,6 +8,7 @@ (:require [app.common.spec :as us] [app.db :as db] + [app.rpc :as-alias rpc] [app.rpc.doc :as-alias doc] [app.util.services :as sv] [clojure.spec.alpha :as s])) @@ -47,18 +48,18 @@ order by f.created_at asc") (defn search-files - [conn {:keys [profile-id team-id search-term] :as params}] + [conn {:keys [::rpc/profile-id team-id search-term] :as params}] (db/exec! conn [sql:search-files profile-id team-id profile-id team-id search-term])) -(s/def ::profile-id ::us/uuid) (s/def ::team-id ::us/uuid) (s/def ::search-files ::us/string) (s/def ::search-files - (s/keys :req-un [::profile-id ::team-id] + (s/keys :req [::rpc/profile-id] + :req-un [::team-id] :opt-un [::search-term])) (sv/defmethod ::search-files diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 5abed23eb..f09ce9ede 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -17,6 +17,7 @@ [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] @@ -35,7 +36,6 @@ (s/def ::id ::us/uuid) (s/def ::name ::us/string) -(s/def ::profile-id ::us/uuid) (s/def ::file-id ::us/uuid) (s/def ::team-id ::us/uuid) @@ -78,11 +78,11 @@ (declare retrieve-teams) (s/def ::get-teams - (s/keys :req-un [::profile-id])) + (s/keys :req [::rpc/profile-id])) (sv/defmethod ::get-teams {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id]}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (with-open [conn (db/open pool)] (retrieve-teams conn profile-id))) @@ -122,11 +122,12 @@ (declare retrieve-team) (s/def ::get-team - (s/keys :req-un [::profile-id ::id])) + (s/keys :req [::rpc/profile-id] + :req-un [::id])) (sv/defmethod ::get-team {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id id]}] + [{:keys [pool] :as cfg} {:keys [::rpc/profile-id id]}] (with-open [conn (db/open pool)] (retrieve-team conn profile-id id))) @@ -161,11 +162,12 @@ (s/def ::team-id ::us/uuid) (s/def ::get-team-members - (s/keys :req-un [::profile-id ::team-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id])) (sv/defmethod ::get-team-members {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [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) (retrieve-team-members conn team-id))) @@ -177,13 +179,13 @@ (declare retrieve-team-for-file) (s/def ::get-team-users - (s/and (s/keys :req-un [::profile-id] + (s/and (s/keys :req [::rpc/profile-id] :opt-un [::team-id ::file-id]) #(or (:team-id %) (:file-id %)))) (sv/defmethod ::get-team-users {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id file-id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}] (with-open [conn (db/open pool)] (if team-id (do @@ -236,11 +238,12 @@ (declare retrieve-team-stats) (s/def ::get-team-stats - (s/keys :req-un [::profile-id ::team-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id])) (sv/defmethod ::get-team-stats {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [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) (retrieve-team-stats conn team-id))) @@ -257,7 +260,8 @@ ;; --- Query: Team invitations (s/def ::get-team-invitations - (s/keys :req-un [::profile-id ::team-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id])) (def sql:team-invitations "select email_to as email, role, (valid_until < now()) as expired @@ -270,7 +274,7 @@ (sv/defmethod ::get-team-invitations {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [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) (get-team-invitations conn team-id))) @@ -285,14 +289,15 @@ (declare ^:private create-team-default-project) (s/def ::create-team - (s/keys :req-un [::profile-id ::name] + (s/keys :req [::rpc/profile-id] + :req-un [::name] :opt-un [::id])) (sv/defmethod ::create-team {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} params] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] - (create-team conn params))) + (create-team conn (assoc params :profile-id profile-id)))) (defn create-team "This is a complete team creation process, it creates the team @@ -316,22 +321,20 @@ :is-default is-default}))) (defn- create-team-role - [conn {:keys [team-id profile-id role] :as params}] + [conn {:keys [profile-id team-id role] :as params}] (let [params {:team-id team-id :profile-id profile-id}] (->> (perms/assign-role-flags params role) (db/insert! conn :team-profile-rel)))) (defn- create-team-default-project - [conn {:keys [team-id profile-id] :as params}] + [conn {:keys [profile-id team-id] :as params}] (let [project {:id (uuid/next) :team-id team-id :name "Drafts" :is-default true} project (create-project conn project)] - (create-project-role conn {:project-id (:id project) - :profile-id profile-id - :role :owner}) + (create-project-role conn profile-id (:id project) :owner) project)) ;; NOTE: we have project creation here because there are cyclic @@ -351,7 +354,7 @@ :is-default is-default}))) (defn create-project-role - [conn {:keys [project-id profile-id role]}] + [conn profile-id project-id role] (let [params {:project-id project-id :profile-id profile-id}] (->> (perms/assign-role-flags params role) @@ -360,11 +363,12 @@ ;; --- Mutation: Update Team (s/def ::update-team - (s/keys :req-un [::profile-id ::name ::id])) + (s/keys :req [::rpc/profile-id] + :req-un [::name ::id])) (sv/defmethod ::update-team {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [id name profile-id] :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) (db/update! conn :team @@ -379,11 +383,12 @@ (s/def ::reassign-to ::us/uuid) (s/def ::leave-team - (s/keys :req-un [::profile-id ::id] + (s/keys :req [::rpc/profile-id] + :req-un [::id] :opt-un [::reassign-to])) (defn leave-team - [conn {:keys [id profile-id reassign-to]}] + [conn {:keys [::rpc/profile-id id reassign-to]}] (let [perms (get-permissions conn profile-id id) members (retrieve-team-members conn id)] @@ -438,7 +443,8 @@ ;; --- Mutation: Delete Team (s/def ::delete-team - (s/keys :req-un [::profile-id ::id])) + (s/keys :req [::rpc/profile-id] + :req-un [::id])) ;; TODO: right now just don't allow delete default team, in future it ;; should raise a specific exception for signal that this action is @@ -446,7 +452,7 @@ (sv/defmethod ::delete-team {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [id profile-id] :as params}] + [{:keys [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) @@ -477,7 +483,7 @@ :viewer {:is-owner false :is-admin false :can-edit false})) (defn update-team-member-role - [conn {:keys [team-id profile-id member-id role] :as params}] + [conn {:keys [profile-id team-id member-id role] :as params}] ;; We retrieve all team members instead of query the ;; database for a single member. This is just for ;; convenience, if this becomes a bottleneck or problematic, @@ -524,23 +530,25 @@ nil))) (s/def ::update-team-member-role - (s/keys :req-un [::profile-id ::team-id ::member-id ::role])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::member-id ::role])) (sv/defmethod ::update-team-member-role {::doc/added "1.17"} [{:keys [::db/pool] :as cfg} params] (db/with-atomic [conn pool] - (update-team-member-role conn params))) + (update-team-member-role conn (assoc params :profile-id (::rpc/profile-id params))))) ;; --- Mutation: Delete Team Member (s/def ::delete-team-member - (s/keys :req-un [::profile-id ::team-id ::member-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::member-id])) (sv/defmethod ::delete-team-member {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [team-id profile-id member-id] :as params}] + [{:keys [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) @@ -564,15 +572,16 @@ (s/def ::file ::media/upload) (s/def ::update-team-photo - (s/keys :req-un [::profile-id ::team-id ::file])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::file])) (sv/defmethod ::update-team-photo {::doc/added "1.17"} - [cfg {:keys [file] :as params}] + [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)] - (update-team-photo cfg params))) + (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}] @@ -632,10 +641,10 @@ update set role = ?, updated_at = now();") (defn- create-invitation-token - [cfg {:keys [expire profile-id team-id member-id member-email role]}] + [cfg {:keys [profile-id valid-until team-id member-id member-email role]}] (tokens/generate (::main/props cfg) {:iss :team-invitation - :exp expire + :exp valid-until :profile-id profile-id :role role :team-id team-id @@ -654,7 +663,7 @@ (let [member (profile/retrieve-profile-data-by-email conn email) expire (dt/in-future "168h") ;; 7 days itoken (create-invitation-token cfg {:profile-id (:id profile) - :expire expire + :valid-until expire :team-id (:id team) :member-email (or (:email member) email) :member-id (:id member) @@ -715,14 +724,15 @@ (s/def ::email ::us/email) (s/def ::emails ::us/set-of-valid-emails) (s/def ::create-team-invitations - (s/keys :req-un [::profile-id ::team-id ::role] + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::role] :opt-un [::email ::emails])) (sv/defmethod ::create-team-invitations "A rpc call that allow to send a single or multiple invitations to join the team." {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}] + [{:keys [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) @@ -760,7 +770,7 @@ (sv/defmethod ::create-team-with-invitations {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}] + [{:keys [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) @@ -791,11 +801,12 @@ ;; --- Query: get-team-invitation-token (s/def ::get-team-invitation-token - (s/keys :req-un [::profile-id ::team-id ::email])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::email])) (sv/defmethod ::get-team-invitation-token {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] (check-read-permissions! pool profile-id team-id) (let [invit (-> (db/get pool :team-invitation {:team-id team-id @@ -804,7 +815,7 @@ member (profile/retrieve-profile-data-by-email pool (:email invit)) token (create-invitation-token cfg {:team-id (:team-id invit) :profile-id profile-id - :expire (:expire invit) + :valid-until (:valid-until invit) :role (:role invit) :member-id (:id member) :member-email (or (:email member) (:email-to invit))})] @@ -813,11 +824,12 @@ ;; --- Mutation: Update invitation role (s/def ::update-team-invitation-role - (s/keys :req-un [::profile-id ::team-id ::email ::role])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::email ::role])) (sv/defmethod ::update-team-invitation-role {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}] + [{:keys [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)] @@ -833,11 +845,12 @@ ;; --- Mutation: Delete invitation (s/def ::delete-team-invitation - (s/keys :req-un [::profile-id ::team-id ::email])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::email])) (sv/defmethod ::delete-team-invitation {::doc/added "1.17"} - [{:keys [pool] :as cfg} {:keys [profile-id team-id email] :as params}] + [{:keys [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 66fce865d..b9b1673d2 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.rpc :as-alias rpc] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] @@ -27,10 +28,10 @@ (s/def ::verify-token (s/keys :req-un [::token] - :opt-un [::profile-id])) + :opt [::rpc/profile-id])) (sv/defmethod ::verify-token - {:auth false + {::rpc/auth false ::doc/added "1.15"} [{:keys [pool sprops] :as cfg} {:keys [token] :as params}] (db/with-atomic [conn pool] @@ -126,7 +127,8 @@ :opt-un [::spec.team-invitation/member-id])) (defmethod process-token :team-invitation - [{:keys [conn session] :as cfg} {:keys [profile-id token]} + [{:keys [conn session] :as cfg} + {:keys [::rpc/profile-id token]} {:keys [member-id team-id member-email] :as claims}] (us/verify! ::team-invitation-claims claims) diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index 68cd69f88..b68dc7e53 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -8,6 +8,7 @@ (:require [app.common.exceptions :as ex] [app.db :as db] + [app.rpc :as-alias rpc] [app.rpc.commands.comments :as comments] [app.rpc.commands.files :as files] [app.rpc.cond :as-alias cond] @@ -73,16 +74,16 @@ (s/def ::get-view-only-bundle (s/keys :req-un [::files/file-id] - :opt-un [::files/profile-id - ::files/share-id - ::files/features])) + :opt-un [::files/share-id + ::files/features] + :opt [::rpc/profile-id])) (sv/defmethod ::get-view-only-bundle - {:auth false + {::rpc/auth false ::cond/get-object #(files/get-minimal-file %1 (:file-id %2)) ::cond/key-fn files/get-file-etag ::cond/reuse-key? true ::doc/added "1.17"} [{:keys [pool]} params] (with-open [conn (db/open pool)] - (get-view-only-bundle conn params))) + (get-view-only-bundle conn (assoc params :profile-id (::rpc/profile-id params))))) diff --git a/backend/src/app/rpc/commands/webhooks.clj b/backend/src/app/rpc/commands/webhooks.clj index 13f7578d4..2786b80e6 100644 --- a/backend/src/app/rpc/commands/webhooks.clj +++ b/backend/src/app/rpc/commands/webhooks.clj @@ -12,6 +12,7 @@ [app.db :as db] [app.http.client :as http] [app.loggers.webhooks :as webhooks] + [app.rpc :as-alias rpc] [app.rpc.commands.teams :refer [check-edition-permissions! check-read-permissions!]] [app.rpc.doc :as-alias doc] [app.util.services :as sv] @@ -23,7 +24,6 @@ ;; --- Mutation: Create Webhook -(s/def ::profile-id ::us/uuid) (s/def ::team-id ::us/uuid) (s/def ::uri ::us/not-empty-string) (s/def ::is-active ::us/boolean) @@ -33,7 +33,8 @@ "application/transit+json"}) (s/def ::create-webhook - (s/keys :req-un [::profile-id ::team-id ::uri ::mtype] + (s/keys :req [::rpc/profile-id] + :req-un [::team-id ::uri ::mtype] :opt-un [::is-active])) ;; NOTE: for now the quote is hardcoded but this need to be solved in @@ -98,7 +99,7 @@ (sv/defmethod ::create-webhook {::doc/added "1.17"} - [{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id team-id] :as params}] + [{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] (check-edition-permissions! pool profile-id team-id) (validate-quotes! cfg params) (->> (validate-webhook! cfg nil params) @@ -109,18 +110,19 @@ (sv/defmethod ::update-webhook {::doc/added "1.17"} - [{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [id profile-id] :as params}] + [{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id id] :as params}] (let [whook (db/get pool :webhook {:id id})] (check-edition-permissions! pool profile-id (:team-id whook)) (->> (validate-webhook! cfg whook params) (p/fmap executor (fn [_] (update-webhook! cfg whook params)))))) (s/def ::delete-webhook - (s/keys :req-un [::profile-id ::id])) + (s/keys :req [::rpc/profile-id] + :req-un [::id])) (sv/defmethod ::delete-webhook {::doc/added "1.17"} - [{:keys [::db/pool] :as cfg} {:keys [profile-id id]}] + [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}] (db/with-atomic [conn pool] (let [whook (db/get conn :webhook {:id id})] (check-edition-permissions! conn profile-id (:team-id whook)) @@ -131,14 +133,15 @@ (s/def ::team-id ::us/uuid) (s/def ::get-webhooks - (s/keys :req-un [::profile-id ::team-id])) + (s/keys :req [::rpc/profile-id] + :req-un [::team-id])) (def sql:get-webhooks "select id, uri, mtype, is_active, error_code, error_count from webhook where team_id = ? order by uri") (sv/defmethod ::get-webhooks - [{:keys [pool] :as cfg} {:keys [profile-id team-id]}] + [{:keys [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]))) diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj index 48dfb39ef..273b9313b 100644 --- a/backend/src/app/rpc/mutations/files.clj +++ b/backend/src/app/rpc/mutations/files.clj @@ -81,7 +81,8 @@ (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))) + (cmd.files/mark-file-deleted conn params) + nil)) ;; --- Mutation: Link file to library diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index e1f07f598..8112bb661 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -6,6 +6,7 @@ (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] @@ -17,7 +18,7 @@ [app.media :as media] [app.rpc :as-alias rpc] [app.rpc.climit :as-alias climit] - [app.rpc.commands.auth :as auth] + [app.rpc.commands.auth :as cmd.auth] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] @@ -182,7 +183,7 @@ (defn- change-email-immediately [{:keys [conn]} {:keys [profile email] :as params}] (when (not= email (:email profile)) - (auth/check-profile-existence! conn params)) + (cmd.auth/check-profile-existence! conn params)) (db/update! conn :profile {:email email} {:id (:id profile)}) @@ -201,7 +202,7 @@ :exp (dt/in-future {:days 30})})] (when (not= email (:email profile)) - (auth/check-profile-existence! conn params)) + (cmd.auth/check-profile-existence! conn params)) (when-not (eml/allow-send-emails? conn profile) (ex/raise :type :validation diff --git a/backend/src/app/rpc/mutations/projects.clj b/backend/src/app/rpc/mutations/projects.clj index 95fbb5da9..9c9188217 100644 --- a/backend/src/app/rpc/mutations/projects.clj +++ b/backend/src/app/rpc/mutations/projects.clj @@ -26,8 +26,6 @@ ;; --- Mutation: Create Project -(declare create-project-profile-state) - (s/def ::team-id ::us/uuid) (s/def ::create-project (s/keys :req-un [::profile-id ::team-id ::name] @@ -39,21 +37,16 @@ [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] (db/with-atomic [conn pool] (teams/check-edition-permissions! conn profile-id team-id) - (let [project (teams/create-project conn params) - params (assoc params - :project-id (:id project) - :role :owner)] - (teams/create-project-role conn params) - (create-project-profile-state conn params) - (assoc project :is-pinned true)))) + (let [project (teams/create-project conn params)] + (teams/create-project-role conn profile-id (:id project) :owner) -(defn create-project-profile-state - [conn {:keys [team-id project-id profile-id] :as params}] - (db/insert! conn :team-project-profile-rel - {:project-id project-id - :profile-id profile-id - :team-id team-id - :is-pinned true})) + (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 @@ -122,4 +115,7 @@ {:deleted-at (dt/now)} {:id id :is-default false})] (rph/with-meta (rph/wrap) - {::audit/props {:team-id (:team-id project)}})))) + {::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/queries/profile.clj b/backend/src/app/rpc/queries/profile.clj index eff758aa9..1d5d605cc 100644 --- a/backend/src/app/rpc/queries/profile.clj +++ b/backend/src/app/rpc/queries/profile.clj @@ -10,6 +10,7 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.db :as db] + [app.rpc :as-alias rpc] [app.util.services :as sv] [clojure.spec.alpha :as s] [cuerdas.core :as str])) @@ -36,7 +37,7 @@ (s/keys :opt-un [::profile-id])) (sv/defmethod ::profile - {:auth false} + {::rpc/auth false} [{:keys [pool] :as cfg} {:keys [profile-id] :as params}] ;; 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 diff --git a/backend/src/app/rpc/queries/viewer.clj b/backend/src/app/rpc/queries/viewer.clj index 5c2531570..837dab94f 100644 --- a/backend/src/app/rpc/queries/viewer.clj +++ b/backend/src/app/rpc/queries/viewer.clj @@ -8,6 +8,7 @@ (:require [app.common.spec :as us] [app.db :as db] + [app.rpc :as-alias rpc] [app.rpc.commands.viewer :as viewer] [app.rpc.doc :as-alias doc] [app.util.services :as sv] @@ -19,7 +20,7 @@ (s/keys :opt-un [::components-v2]))) (sv/defmethod ::view-only-bundle - {:auth false + {::rpc/auth false ::doc/added "1.3" ::doc/deprecated "1.17"} [{:keys [pool] :as cfg} {:keys [features components-v2] :as params}] diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj index d5cb1a258..8ecc2696f 100644 --- a/backend/src/app/setup.clj +++ b/backend/src/app/setup.clj @@ -13,6 +13,7 @@ [app.db :as db] [app.main :as-alias main] [app.setup.builtin-templates] + [app.setup.initial-user] [app.setup.keys :as keys] [buddy.core.codecs :as bc] [buddy.core.nonce :as bn] diff --git a/backend/src/app/setup/initial_user.clj b/backend/src/app/setup/initial_user.clj new file mode 100644 index 000000000..35d254b38 --- /dev/null +++ b/backend/src/app/setup/initial_user.clj @@ -0,0 +1,40 @@ +;; 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.setup.initial-user + "Initial data setup of instance." + (:require + [app.auth :as auth] + [app.common.logging :as l] + [app.config :as cf] + [app.db :as db] + [app.setup :as-alias setup] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(def ^:private sql:insert-profile + "insert into profile (id, fullname, email, password, is_active, is_admin, created_at, modified_at) + values ('00000000-0000-0000-0000-000000000000', 'Admin', ?, ?, true, true, now(), now()) + on conflict (id) + do update set email = ?, password = ?") + +(defmethod ig/pre-init-spec ::setup/initial-profile [_] + (s/keys :req [::db/pool])) + +(defmethod ig/init-key ::setup/initial-profile + [_ {:keys [::db/pool]}] + (let [email (cf/get :setup-admin-email) + password (cf/get :setup-admin-password)] + (when (and email password) + (db/with-atomic [conn pool] + (let [pwd (auth/derive-password password)] + (db/exec-one! conn [sql:insert-profile email pwd email pwd]) + (l/info :hint "setting initial user (admin)" + :email email + :password "********")))) + nil)) + + diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index 5da3f6c19..d37d0f520 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -8,6 +8,7 @@ "A main namespace for server repl." #_:clj-kondo/ignore (:require + [app.auth :refer [derive-password]] [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] @@ -20,7 +21,6 @@ [app.db :as db] [app.db.sql :as sql] [app.main :refer [system]] - [app.rpc.commands.auth :refer [derive-password]] [app.rpc.queries.profile :as prof] [app.util.blob :as blob] [app.util.time :as dt] diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj index c90d1909f..eacd52341 100644 --- a/backend/src/app/tasks/objects_gc.clj +++ b/backend/src/app/tasks/objects_gc.clj @@ -6,7 +6,7 @@ (ns app.tasks.objects-gc "A maintenance task that performs a general purpose garbage collection - of deleted objects." + of deleted or unreachable objects." (:require [app.common.data :as d] [app.common.logging :as l] @@ -16,154 +16,247 @@ [app.storage :as sto] [app.util.time :as dt] [clojure.spec.alpha :as s] - [cuerdas.core :as str] [integrant.core :as ig])) -(def target-tables - ["profile" - "team" - "file" - "project" - "team_font_variant"]) - -(defmulti delete-objects :table) - -(def sql:delete-objects - "with deleted as ( - select id from %(table)s - where deleted_at is not null - and deleted_at < now() - ?::interval - order by deleted_at - limit %(limit)s - ) - delete from %(table)s - where id in (select id from deleted) - returning *") - -;; --- IMPL: generic object deletion - -(defmethod delete-objects :default - [{:keys [conn min-age table] :as cfg}] - (let [sql (str/fmt sql:delete-objects - {:table table :limit 50}) - result (db/exec! conn [sql min-age])] - - (doseq [{:keys [id] :as item} result] - (l/debug :hint "permanently delete object" :table table :id id)) - - (count result))) - -;; --- IMPL: file deletion - -(defmethod delete-objects "file" - [{:keys [conn min-age table] :as cfg}] - (let [sql (str/fmt sql:delete-objects {:table table :limit 50}) - result (db/exec! conn [sql min-age])] - - (doseq [{:keys [id] :as item} result] - (l/debug :hint "permanently delete object" :table table :id id)) - - (count result))) - -;; --- IMPL: team-font-variant deletion - -(defmethod delete-objects "team_font_variant" - [{:keys [conn min-age storage table] :as cfg}] - (let [sql (str/fmt sql:delete-objects {:table table :limit 50}) - fonts (db/exec! conn [sql min-age]) - storage (media/configure-assets-storage storage conn)] - (doseq [{:keys [id] :as font} fonts] - (l/debug :hint "permanently delete object" :table table :id id) - (some->> (:woff1-file-id font) (sto/touch-object! storage) deref) - (some->> (:woff2-file-id font) (sto/touch-object! storage) deref) - (some->> (:otf-file-id font) (sto/touch-object! storage) deref) - (some->> (:ttf-file-id font) (sto/touch-object! storage) deref)) - (count fonts))) - -;; --- IMPL: team deletion - -(defmethod delete-objects "team" - [{:keys [conn min-age storage table] :as cfg}] - (let [sql (str/fmt sql:delete-objects {:table table :limit 50}) - teams (db/exec! conn [sql min-age]) - storage (media/configure-assets-storage storage conn)] - - (doseq [{:keys [id] :as team} teams] - (l/debug :hint "permanently delete object" :table table :id id) - (some->> (:photo-id team) (sto/touch-object! storage) deref)) - - (count teams))) - -;; --- IMPL: profile deletion - -(def sql:retrieve-deleted-profiles - "select id, photo_id from profile - where deleted_at is not null - and deleted_at < now() - ?::interval - order by deleted_at - limit ? - for update") - -(defmethod delete-objects "profile" - [{:keys [conn min-age storage table] :as cfg}] - (let [profiles (db/exec! conn [sql:retrieve-deleted-profiles min-age 50]) - storage (media/configure-assets-storage storage conn)] - - (doseq [{:keys [id] :as profile} profiles] - (l/debug :hint "permanently delete object" :table table :id id) - - ;; Mark as deleted the storage object related with the photo-id - ;; field. - (some->> (:photo-id profile) (sto/touch-object! storage) deref) - - ;; And finally, permanently delete the profile. - (db/delete! conn :profile {:id id})) - - (count profiles))) - -;; --- INIT - -(defn- process-table - [{:keys [table] :as cfg}] - (loop [n 0] - (let [res (delete-objects cfg)] - (if (pos? res) - (recur (+ n res)) - (do - (l/debug :hint "delete summary" :table table :total n) - n))))) +(declare ^:private delete-profiles!) +(declare ^:private delete-teams!) +(declare ^:private delete-fonts!) +(declare ^:private delete-projects!) +(declare ^:private delete-files!) +(declare ^:private delete-orphan-teams!) (s/def ::min-age ::dt/duration) (defmethod ig/pre-init-spec ::handler [_] - (s/keys :req-un [::db/pool ::sto/storage] - :opt-un [::min-age])) + (s/keys :req [::db/pool ::sto/storage] + :opt [::min-age])) (defmethod ig/prep-key ::handler [_ cfg] - (merge {:min-age cf/deletion-delay} + (merge {::min-age cf/deletion-delay} (d/without-nils cfg))) (defmethod ig/init-key ::handler - [_ {:keys [pool] :as cfg}] + [_ {:keys [::db/pool ::sto/storage] :as cfg}] (fn [params] (db/with-atomic [conn pool] - (let [min-age (or (:min-age params) (:min-age cfg)) + (let [min-age (or (:min-age params) (::min-age cfg)) + _ (l/info :hint "gc started" + :min-age (dt/format-duration min-age) + :rollback? (boolean (:rollback? params))) + + storage (media/configure-assets-storage storage conn) cfg (-> cfg - (assoc :min-age (db/interval min-age)) - (assoc :conn conn))] - (loop [tables (seq target-tables) - total 0] - (if-let [table (first tables)] - (recur (rest tables) - (+ total (process-table (assoc cfg :table table)))) - (do - (l/info :hint "objects gc finished successfully" - :min-age (dt/format-duration min-age) - :total total) + (assoc ::min-age (db/interval min-age)) + (assoc ::conn conn) + (assoc ::storage storage)) - (when (:rollback? params) - (db/rollback! conn)) + htotal (+ (delete-profiles! cfg) + (delete-teams! cfg) + (delete-projects! cfg) + (delete-files! cfg) + (delete-fonts! cfg)) + stotal (delete-orphan-teams! cfg)] - {:processed total}))))))) + (l/info :hint "gc finished" + :deleted htotal + :orphans stotal + :rollback? (boolean (:rollback? params))) + (when (:rollback? params) + (db/rollback! conn)) + + {:processed (+ stotal htotal)})))) + + +(def ^:private sql:get-profiles-chunk + "select id, photo_id, created_at from profile + where deleted_at is not null + and deleted_at < now() - ?::interval + and created_at < ? + order by created_at desc + limit 10 + for update skip locked") + +(defn- delete-profiles! + [{:keys [::conn ::min-age ::storage] :as cfg}] + (letfn [(get-chunk [cursor] + (let [rows (db/exec! conn [sql:get-profiles-chunk min-age cursor])] + [(some->> rows peek :created-at) rows]))] + (reduce + (fn [total {:keys [id photo-id]}] + (l/debug :hint "permanently delete profile" :id (str id)) + + ;; Mark as deleted the storage object related with the + ;; photo-id field. + (some->> photo-id (sto/touch-object! storage) deref) + + ;; And finally, permanently delete the profile. + (db/delete! conn :profile {:id id}) + + (inc total)) + 0 + (d/iteration get-chunk + :vf second + :kf first + :initk (dt/now))))) + +(def ^:private sql:get-teams-chunk + "select id, photo_id, created_at from team + where deleted_at is not null + and deleted_at < now() - ?::interval + and created_at < ? + order by created_at desc + limit 10 + for update skip locked") + +(defn- delete-teams! + [{:keys [::conn ::min-age ::storage] :as cfg}] + (letfn [(get-chunk [cursor] + (let [rows (db/exec! conn [sql:get-teams-chunk min-age cursor])] + [(some->> rows peek :created-at) rows]))] + (reduce + (fn [total {:keys [id photo-id]}] + (l/debug :hint "permanently delete team" :id (str id)) + + ;; Mark as deleted the storage object related with the + ;; photo-id field. + (some->> photo-id (sto/touch-object! storage) deref) + + ;; And finally, permanently delete the team. + (db/delete! conn :team {:id id}) + + (inc total)) + 0 + (d/iteration get-chunk + :vf second + :kf first + :initk (dt/now))))) + + +(def ^:private sql:get-orphan-teams-chunk + "select t.id, t.created_at + from team as t + left join team_profile_rel as tpr + on (t.id = tpr.team_id) + where tpr.profile_id is null + and t.created_at < ? + order by t.created_at desc + limit 10 + for update of t skip locked;") + +(defn- delete-orphan-teams! + "Find all orphan teams (with no members and mark them for + deletion (soft delete)." + [{:keys [::conn] :as cfg}] + (letfn [(get-chunk [cursor] + (let [rows (db/exec! conn [sql:get-orphan-teams-chunk cursor])] + [(some->> rows peek :created-at) rows]))] + (reduce + (fn [total {:keys [id]}] + (l/debug :hint "mark team for deletion" :id (str id)) + + ;; And finally, permanently delete the team. + (db/update! conn :team + {:deleted-at (dt/now)} + {:id id}) + + (inc total)) + 0 + (d/iteration get-chunk + :vf second + :kf first + :initk (dt/now))))) + +(def ^:private sql:get-fonts-chunk + "select id, created_at, woff1_file_id, woff2_file_id, otf_file_id, ttf_file_id + from team_font_variant + where deleted_at is not null + and deleted_at < now() - ?::interval + and created_at < ? + order by created_at desc + limit 10 + for update skip locked") + +(defn- delete-fonts! + [{:keys [::conn ::min-age ::storage] :as cfg}] + (letfn [(get-chunk [cursor] + (let [rows (db/exec! conn [sql:get-fonts-chunk min-age cursor])] + [(some->> rows peek :created-at) rows]))] + (reduce + (fn [total {:keys [id] :as font}] + (l/debug :hint "permanently delete font variant" :id (str id)) + + ;; Mark as deleted the all related storage objects + (some->> (:woff1-file-id font) (sto/touch-object! storage) deref) + (some->> (:woff2-file-id font) (sto/touch-object! storage) deref) + (some->> (:otf-file-id font) (sto/touch-object! storage) deref) + (some->> (:ttf-file-id font) (sto/touch-object! storage) deref) + + ;; And finally, permanently delete the team font variant + (db/delete! conn :team-font-variant {:id id}) + + (inc total)) + 0 + (d/iteration get-chunk + :vf second + :kf first + :initk (dt/now))))) + +(def ^:private sql:get-projects-chunk + "select id, created_at + from project + where deleted_at is not null + and deleted_at < now() - ?::interval + and created_at < ? + order by created_at desc + limit 10 + for update skip locked") + +(defn- delete-projects! + [{:keys [::conn ::min-age] :as cfg}] + (letfn [(get-chunk [cursor] + (let [rows (db/exec! conn [sql:get-projects-chunk min-age cursor])] + [(some->> rows peek :created-at) rows]))] + (reduce + (fn [total {:keys [id]}] + (l/debug :hint "permanently delete project" :id (str id)) + + ;; And finally, permanently delete the project. + (db/delete! conn :project {:id id}) + + (inc total)) + 0 + (d/iteration get-chunk + :vf second + :kf first + :initk (dt/now))))) + +(def ^:private sql:get-files-chunk + "select id, created_at + from file + where deleted_at is not null + and deleted_at < now() - ?::interval + and created_at < ? + order by created_at desc + limit 10 + for update skip locked") + +(defn- delete-files! + [{:keys [::conn ::min-age] :as cfg}] + (letfn [(get-chunk [cursor] + (let [rows (db/exec! conn [sql:get-files-chunk min-age cursor])] + [(some->> rows peek :created-at) rows]))] + (reduce + (fn [total {:keys [id]}] + (l/debug :hint "permanently delete file" :id (str id)) + + ;; And finally, permanently delete the file. + (db/delete! conn :file {:id id}) + + (inc total)) + 0 + (d/iteration get-chunk + :vf second + :kf first + :initk (dt/now))))) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 7b2792ca4..1fa09c4d7 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -6,6 +6,7 @@ (ns backend-tests.helpers (:require + [app.auth] [app.common.data :as d] [app.common.flags :as flags] [app.common.pages :as cp] @@ -17,6 +18,7 @@ [app.main :as main] [app.media] [app.migrations] + [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] @@ -101,8 +103,9 @@ *pool* (:app.db/pool system)] (with-redefs [app.config/flags (flags/parse flags/default default-flags (:flags config)) app.config/config config - app.rpc.commands.auth/derive-password identity - app.rpc.commands.auth/verify-password (fn [a b] {:valid (= a b)})] + 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))))) @@ -322,14 +325,21 @@ (try-on! (method-fn (dissoc data ::type))))) (defn mutation! - [{:keys [::type] :as data}] + [{:keys [::type profile-id] :as data}] (let [method-fn (get-in *system* [:app.rpc/methods :mutations type])] - (try-on! (method-fn (dissoc data ::type))))) + (try-on! (method-fn (-> data + (dissoc ::type) + (assoc ::rpc/profile-id profile-id) + (d/without-nils)))))) (defn query! - [{:keys [::type] :as data}] + [{:keys [::type profile-id] :as data}] (let [method-fn (get-in *system* [:app.rpc/methods :queries type])] - (try-on! (method-fn (dissoc data ::type))))) + (try-on! (method-fn (-> data + (dissoc ::type) + (assoc ::rpc/profile-id profile-id) + (d/without-nils)))))) + (defn run-task! ([name] diff --git a/backend/test/backend_tests/loggers_webhooks_test.clj b/backend/test/backend_tests/loggers_webhooks_test.clj index 8af5bd780..fac41f78b 100644 --- a/backend/test/backend_tests/loggers_webhooks_test.clj +++ b/backend/test/backend_tests/loggers_webhooks_test.clj @@ -65,8 +65,7 @@ ;; Refresh webhook (let [whk' (th/db-get :webhook {:id (:id whk)})] - (t/is (nil? (:error-code whk'))) - (prn whk')) + (t/is (nil? (:error-code whk')))) ))) diff --git a/backend/test/backend_tests/rpc_audit_test.clj b/backend/test/backend_tests/rpc_audit_test.clj index 9df795192..b27aa0e2c 100644 --- a/backend/test/backend_tests/rpc_audit_test.clj +++ b/backend/test/backend_tests/rpc_audit_test.clj @@ -10,6 +10,7 @@ [app.common.uuid :as uuid] [app.db :as db] [app.util.time :as dt] + [app.rpc :as-alias rpc] [backend-tests.helpers :as th] [clojure.test :as t])) @@ -37,7 +38,7 @@ params {::th/type :push-audit-events :app.http/request http-request - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :events [{:name "navigate" :props {:project-id proj-id :team-id team-id @@ -67,7 +68,7 @@ params {::th/type :push-audit-events :app.http/request http-request - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :events [{:name "navigate" :props {:project-id proj-id :team-id team-id diff --git a/backend/test/backend_tests/rpc_cond_middleware_test.clj b/backend/test/backend_tests/rpc_cond_middleware_test.clj index 74f95e196..58978e3bc 100644 --- a/backend/test/backend_tests/rpc_cond_middleware_test.clj +++ b/backend/test/backend_tests/rpc_cond_middleware_test.clj @@ -6,12 +6,13 @@ (ns backend-tests.rpc-cond-middleware-test (:require - [backend-tests.storage-test :refer [configure-storage-backend]] - [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] + [app.rpc :as-alias rpc] [app.rpc.cond :as cond] + [backend-tests.helpers :as th] + [backend-tests.storage-test :refer [configure-storage-backend]] [clojure.test :as t] [datoteka.core :as fs])) @@ -24,7 +25,9 @@ :profile-id (:id profile)}) file1 (th/create-file* 1 {:profile-id (:id profile) :project-id (:id project)}) - params {::th/type :get-file :id (:id file1) :profile-id (:id profile)}] + params {::th/type :get-file + :id (:id file1) + ::rpc/profile-id (:id profile)}] (binding [cond/*enabled* true] (let [{:keys [error result]} (th/command! params)] diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 6f7562b54..068aa5a51 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -583,6 +583,7 @@ :object-id (str page-id frame1-id) :data nil} {:keys [error result] :as out} (th/mutation! data)] + ;; (th/print-result! out) (t/is (nil? error)) (t/is (nil? result))) diff --git a/backend/test/backend_tests/rpc_management_test.clj b/backend/test/backend_tests/rpc_management_test.clj index 5b4f26d05..e3e0ddbd3 100644 --- a/backend/test/backend_tests/rpc_management_test.clj +++ b/backend/test/backend_tests/rpc_management_test.clj @@ -6,12 +6,13 @@ (ns backend-tests.rpc-management-test (:require - [backend-tests.storage-test :refer [configure-storage-backend]] - [backend-tests.helpers :as th] [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] + [backend-tests.storage-test :refer [configure-storage-backend]] [buddy.core.bytes :as b] [clojure.test :as t] [datoteka.core :as fs])) @@ -50,7 +51,7 @@ :object (select-keys mobj [:id :width :height :mtype :name])}]}) (let [data {::th/type :duplicate-file - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :file-id (:id file1) :name "file 1 (copy)"} out (th/command! data)] @@ -122,7 +123,7 @@ @(sto/del-object! storage sobject) (let [data {::th/type :duplicate-file - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :file-id (:id file1) :name "file 1 (copy)"} out (th/command! data)] @@ -184,7 +185,7 @@ (let [data {::th/type :duplicate-project - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :project-id (:id project) :name "project 1 (copy)"} out (th/command! data)] @@ -250,7 +251,7 @@ (th/mark-file-deleted* {:id (:id file1)}) (let [data {::th/type :duplicate-project - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :project-id (:id project) :name "project 1 (copy)"} out (th/command! data)] @@ -313,7 +314,7 @@ ;; Try to move to same project (let [data {::th/type :move-files - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :project-id (:id project1) :ids #{(:id file1)}} @@ -333,7 +334,7 @@ ;; move a file1 to project2 (in the same team) (let [data {::th/type :move-files - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :project-id (:id project2) :ids #{(:id file1)}} @@ -416,7 +417,7 @@ ;; move to other project in other team (let [data {::th/type :move-files - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :project-id (:id project2) :ids #{(:id file1)}} out (th/command! data)] @@ -489,7 +490,7 @@ ;; move the library to other project (let [data {::th/type :move-files - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :project-id (:id project2) :ids #{(:id file2)}} out (th/command! data)] @@ -575,7 +576,7 @@ ;; move project1 to other team ;; TODO: correct team change of project (let [data {::th/type :move-project - :profile-id (:id profile) + ::rpc/profile-id (:id profile) :project-id (:id project1) :team-id (:id team)} out (th/command! data)] @@ -608,7 +609,7 @@ (t/deftest clone-template (let [prof (th/create-profile* 1 {:is-active true}) data {::th/type :clone-template - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :project-id (:default-project-id prof) :template-id "test"} @@ -624,7 +625,7 @@ (t/deftest retrieve-list-of-buitin-templates (let [prof (th/create-profile* 1 {:is-active true}) data {::th/type :retrieve-list-of-builtin-templates - :profile-id (:id prof)} + ::rpc/profile-id (:id prof)} out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:error out))) diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 940b2e7a6..56a67c029 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -146,7 +146,12 @@ ;; execute permanent deletion task (let [result (th/run-task! :objects-gc {:min-age (dt/duration "-1m")})] - (t/is (= 1 (:processed result)))) + (t/is (= 2 (:processed result)))) + + (let [row (th/db-get :team + {:id (:default-team-id prof)} + {:check-deleted? false})] + (t/is (dt/instant? (:deleted-at row)))) ;; query profile after delete (let [params {::th/type :profile diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index de66c2436..11ec08a88 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -6,13 +6,14 @@ (ns backend-tests.rpc-team-test (:require - [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.db :as db] [app.http :as http] + [app.rpc :as-alias rpc] [app.storage :as sto] [app.tokens :as tokens] [app.util.time :as dt] + [backend-tests.helpers :as th] [clojure.test :as t] [datoteka.core :as fs] [mockery.core :refer [with-mocks]])) @@ -65,7 +66,7 @@ ;; get invitation token (let [params {::th/type :get-team-invitation-token - :profile-id (:id profile1) + ::rpc/profile-id (:id profile1) :team-id (:id team) :email "foo@bar.com"} out (th/command! params)] @@ -214,7 +215,7 @@ :role "editor" :valid-until (dt/in-future "48h")}) - (let [data {::th/type :verify-token :token token :profile-id (:id profile2)} + (let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile2)} out (th/command! data)] ;; (th/print-result! out) (t/is (th/success? out)) @@ -235,7 +236,7 @@ :role "editor" :valid-until (dt/in-future "48h")}) - (let [data {::th/type :verify-token :token token :profile-id (:id profile1)} + (let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile1)} out (th/command! data)] ;; (th/print-result! out) (t/is (not (th/success? out))) diff --git a/backend/test/backend_tests/rpc_viewer_test.clj b/backend/test/backend_tests/rpc_viewer_test.clj index 6b0ded158..abbf5f206 100644 --- a/backend/test/backend_tests/rpc_viewer_test.clj +++ b/backend/test/backend_tests/rpc_viewer_test.clj @@ -100,6 +100,7 @@ out (th/query! data)] ;; (th/print-result! out) + (t/is (nil? (:error out))) (let [result (:result out)] (t/is (contains? result :file)) (t/is (contains? result :project))))) diff --git a/backend/test/backend_tests/rpc_webhooks_test.clj b/backend/test/backend_tests/rpc_webhooks_test.clj index 6d2f97a17..9f50e724b 100644 --- a/backend/test/backend_tests/rpc_webhooks_test.clj +++ b/backend/test/backend_tests/rpc_webhooks_test.clj @@ -10,6 +10,7 @@ [app.db :as db] [app.http :as http] [app.storage :as sto] + [app.rpc :as-alias rpc] [backend-tests.helpers :as th] [clojure.test :as t] [mockery.core :refer [with-mocks]])) @@ -28,7 +29,7 @@ (t/testing "create webhook" (let [params {::th/type :create-webhook - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :team-id team-id :uri "http://example.com" :mtype "application/json"} @@ -54,7 +55,7 @@ (t/testing "update webhook 1 (success)" (let [params {::th/type :update-webhook - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :id (:id @whook) :uri (:uri @whook) :mtype "application/transit+json" @@ -82,7 +83,7 @@ (t/testing "update webhook 2 (change uri)" (let [params {::th/type :update-webhook - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :id (:id @whook) :uri (str (:uri @whook) "/test") :mtype "application/transit+json" @@ -97,7 +98,7 @@ (t/testing "update webhook 3 (not authorized)" (let [params {::th/type :update-webhook - :profile-id uuid/zero + ::rpc/profile-id uuid/zero :id (:id @whook) :uri (str (:uri @whook) "/test") :mtype "application/transit+json" @@ -115,7 +116,7 @@ (t/testing "delete webhook (success)" (let [params {::th/type :delete-webhook - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :id (:id @whook)} out (th/command! params)] @@ -128,7 +129,7 @@ (t/testing "delete webhook (unauthorozed)" (let [params {::th/type :delete-webhook - :profile-id uuid/zero + ::rpc/profile-id uuid/zero :id (:id @whook)} out (th/command! params)] @@ -149,7 +150,7 @@ (let [prof (th/create-profile* 1 {:is-active true}) team-id (:default-team-id prof) params {::th/type :create-webhook - :profile-id (:id prof) + ::rpc/profile-id (:id prof) :team-id team-id :uri "http://example.com" :mtype "application/json"} diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 4cde8026e..bda243320 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -12,8 +12,7 @@ (def default "A common flags that affects both: backend and frontend." [:enable-registration - :enable-login - :enable-webhooks]) + :enable-login-with-password]) (defn parse [& flags] diff --git a/common/src/app/common/transit.cljc b/common/src/app/common/transit.cljc index ad0ba80d1..9d698cd3a 100644 --- a/common/src/app/common/transit.cljc +++ b/common/src/app/common/transit.cljc @@ -14,6 +14,7 @@ [lambdaisland.uri :as luri] [linked.core :as lk] [linked.set :as lks] + #?(:clj [datoteka.fs :as fs]) #?(:cljs ["luxon" :as lxn])) #?(:clj (:import @@ -22,6 +23,7 @@ java.io.ByteArrayInputStream java.io.ByteArrayOutputStream java.io.File + java.nio.file.Path java.time.Duration java.time.Instant java.time.OffsetDateTime @@ -102,11 +104,15 @@ ;; --- HANDLERS (add-handlers! - #?(:clj - {:id "file" - :class File - :wfn str - :rfn identity}) + #?@(:clj + [{:id "file" + :class File + :wfn str + :rfn identity} + {:id "path" + :class Path + :wfn str + :rfn fs/path}]) #?(:cljs {:id "n" diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 5e2065bb2..0086018b3 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -113,6 +113,10 @@ http { proxy_pass http://127.0.0.1:6060/api; } + location /admin { + proxy_pass http://127.0.0.1:6063/admin; + } + location /webhooks { proxy_pass http://127.0.0.1:6060/webhooks; } diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index 4729d7cdb..d5c727d18 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -1,11 +1,68 @@ +FROM ubuntu:22.04 as jre-build + +ENV DEBIAN_FRONTEND=noninteractive \ + TZ=Etc/UTC + +RUN set -eux; \ + apt-get -qq update; \ + apt-get -qqy --no-install-recommends install \ + curl \ + ca-certificates \ + binutils \ + ; \ + rm -rf /var/lib/apt/lists/*; + +RUN set -eux; \ + ARCH="$(dpkg --print-architecture)"; \ + case "${ARCH}" in \ + aarch64|arm64) \ + ESUM='262be608e266fd76d7496af83b2832be853c3aaf7460d6a4da198cd40db74553'; \ + BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.2.1%2B1/OpenJDK18U-jdk_aarch64_linux_hotspot_18.0.2.1_1.tar.gz'; \ + ;; \ + armhf|armv7l) \ + ESUM='4cd49b92d13847bfad7b3bf635cca349e2c89c7641748c5288bc40d612cdbbd6'; \ + BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.2.1%2B1/OpenJDK18U-jdk_arm_linux_hotspot_18.0.2.1_1.tar.gz'; \ + ;; \ + amd64|x86_64) \ + ESUM='7d6beba8cfc0a8347f278f7414351191a95a707d46b6586e9a786f2669af0f8b'; \ + BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.2.1%2B1/OpenJDK18U-jdk_x64_linux_hotspot_18.0.2.1_1.tar.gz'; \ + ;; \ + *) \ + echo "Unsupported arch: ${ARCH}"; \ + exit 1; \ + ;; \ + esac; \ + curl -LfsSo /tmp/openjdk.tar.gz ${BINARY_URL}; \ + echo "${ESUM} */tmp/openjdk.tar.gz" | sha256sum -c -; \ + mkdir -p /opt/jdk; \ + cd /opt/jdk; \ + tar -xf /tmp/openjdk.tar.gz --strip-components=1; \ + rm -rf /tmp/openjdk.tar.gz; + +RUN /opt/jdk/bin/jlink \ + --verbose \ + --module-path /opt/jdk/jmods \ + --strip-debug \ + --no-man-pages \ + --no-header-files \ + --compress 0 \ + --add-modules java.base,java.naming,java.xml,java.logging,java.net.http,java.sql,java.management,java.desktop,jdk.jfr,jdk.unsupported,jdk.management.jfr \ + --output /opt/jre + + FROM ubuntu:22.04 + LABEL maintainer="Andrey Antukh " +ENV LANG='en_US.UTF-8' \ + LC_ALL='en_US.UTF-8' \ + JAVA_HOME="/opt/jre" \ + PATH=/opt/jre/bin:$PATH \ + TZ=Etc/UTC -ENV LANG='en_US.UTF-8' LC_ALL='en_US.UTF-8' - -WORKDIR /root +COPY --from=jre-build /opt/jre /opt/jre RUN set -ex; \ + useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \ apt-get -qq update; \ apt-get -qqy --no-install-recommends install \ curl \ @@ -23,34 +80,8 @@ RUN set -ex; \ locale-gen; \ rm -rf /var/lib/apt/lists/*; -RUN set -eux; \ - ARCH="$(dpkg --print-architecture)"; \ - case "${ARCH}" in \ - aarch64|arm64) \ - ESUM='37ceaf232a85cce46bcccfd71839854e8b14bf3160e7ef72a676b9cae45ee8af'; \ - BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_linux_hotspot_18.0.1_10.tar.gz'; \ - ;; \ - armhf|armv7l) \ - ESUM='0ddec3c165ab0b662a57a845db3fdaeb840660b493f164696b03df76aadf61c8'; \ - BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_arm_linux_hotspot_18.0.1_10.tar.gz'; \ - ;; \ - amd64|x86_64) \ - ESUM='16b1d9d75f22c157af04a1fd9c664324c7f4b5163c022b382a2f2e8897c1b0a2'; \ - BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz'; \ - ;; \ - *) \ - echo "Unsupported arch: ${ARCH}"; \ - exit 1; \ - ;; \ - esac; \ - curl -LfsSo /tmp/openjdk.tar.gz ${BINARY_URL}; \ - echo "${ESUM} */tmp/openjdk.tar.gz" | sha256sum -c -; \ - mkdir -p /usr/lib/jvm/openjdk; \ - cd /usr/lib/jvm/openjdk; \ - tar -xf /tmp/openjdk.tar.gz --strip-components=1; \ - rm -rf /tmp/openjdk.tar.gz; +COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/ -ENV JAVA_HOME=/usr/lib/jvm/openjdk PATH="/usr/lib/jvm/openjdk/bin:$PATH" -ADD ./bundle-backend/ /opt/penpot/backend/ +USER penpot:penpot WORKDIR /opt/penpot/backend CMD ["/bin/bash", "run.sh"] diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 144b5a9c1..0cc5b2b0a 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -1,70 +1,76 @@ FROM ubuntu:22.04 LABEL maintainer="Andrey Antukh " -ARG DEBIAN_FRONTEND=noninteractive - ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NODE_VERSION=v16.17.0 + NODE_VERSION=v18.12.1 \ + DEBIAN_FRONTEND=noninteractive \ + PATH=/opt/node/bin:$PATH RUN set -ex; \ + useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \ mkdir -p /etc/resolvconf/resolv.conf.d; \ - echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail; \ + echo "nameserver 127.0.0.11" > /etc/resolvconf/resolv.conf.d/tail; \ apt-get -qq update; \ - apt-get -qqy --no-install-recommends install curl tzdata locales ca-certificates fontconfig xz-utils; \ + apt-get -qqy --no-install-recommends install \ + curl \ + tzdata \ + locales \ + ca-certificates \ + fontconfig \ + xz-utils \ + ; \ + rm -rf /var/lib/apt/lists/*; \ echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \ - locale-gen; \ - rm -rf /var/lib/apt/lists/*; + locale-gen; RUN set -ex; \ apt-get -qq update; \ apt-get -qqy install \ - imagemagick \ - ghostscript \ - netpbm \ - poppler-utils \ - potrace \ - gconf-service \ - libasound2 \ - libatk1.0-0 \ - libatk-bridge2.0-0 \ - libatomic1 \ - libcairo2 \ - libcups2 \ - libdbus-1-3 \ - libexpat1 \ - libfontconfig1 \ - libgcc1 \ - libgconf-2-4 \ - libgdk-pixbuf2.0-0 \ - libglib2.0-0 \ - libgtk-3-0 \ - libnspr4 \ - libpango-1.0-0 \ - libpangocairo-1.0-0 \ - libx11-6 \ - libx11-xcb1 \ - libxcb1 \ - libxcb-dri3-0 \ - libxcomposite1 \ - libxcursor1 \ - libxdamage1 \ - libxext6 \ - libxfixes3 \ - libxi6 \ - libxrandr2 \ - libxrender1 \ - libxshmfence1 \ - libxss1 \ - libxtst6 \ - fonts-liberation \ - libnss3 \ - libgbm1 \ + imagemagick \ + ghostscript \ + netpbm \ + poppler-utils \ + potrace \ + gconf-service \ + libasound2 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libatomic1 \ + libcairo2 \ + libcups2 \ + libdbus-1-3 \ + libexpat1 \ + libfontconfig1 \ + libgcc1 \ + libgconf-2-4 \ + libgdk-pixbuf2.0-0 \ + libglib2.0-0 \ + libgtk-3-0 \ + libnspr4 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcb-dri3-0 \ + libxcomposite1 \ + libxcursor1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxi6 \ + libxrandr2 \ + libxrender1 \ + libxshmfence1 \ + libxss1 \ + libxtst6 \ + fonts-liberation \ + libnss3 \ + libgbm1 \ ; \ rm -rf /var/lib/apt/lists/*; -ENV PATH="/usr/local/nodejs/bin:$PATH" - RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ @@ -83,19 +89,22 @@ RUN set -eux; \ ;; \ esac; \ curl -LfsSo /tmp/nodejs.tar.xz ${BINARY_URL}; \ - mkdir -p /usr/local/nodejs; \ - cd /usr/local/nodejs; \ + mkdir -p /opt/node; \ + cd /opt/node; \ tar -xf /tmp/nodejs.tar.xz --strip-components=1; \ - chown -R root /usr/local/nodejs; \ - /usr/local/nodejs/bin/npm install -g yarn; \ - rm -rf /tmp/nodejs.tar.xz; + chown -R root /opt/node; \ + npm install -g yarn; \ + rm -rf /tmp/nodejs.tar.xz; \ + mkdir -p /opt/penpot; \ + chown -R penpot:penpot /opt/penpot; -WORKDIR /opt/app +ADD --chown=penpot:penpot ./bundle-exporter/ /opt/penpot/exporter -ADD ./bundle-exporter/ /opt/app/ +WORKDIR /opt/penpot/exporter +USER penpot:penpot RUN set -ex; \ yarn; \ - npx playwright install chromium; + yarn run playwright install chromium; -CMD ["/usr/local/nodejs/bin/node", "app.js"] +CMD ["node", "app.js"] diff --git a/docker/images/config.env b/docker/images/config.env deleted file mode 100644 index 7744ded6e..000000000 --- a/docker/images/config.env +++ /dev/null @@ -1,96 +0,0 @@ -## Should be set to the public domain where penpot is going to be served. -## -## NOTE: If you are going to serve it under different domain than -## 'localhost' without HTTPS, consider setting the -## `disable-secure-session-cookies' flag on the 'PENPOT_FLAGS' -## setting. - -PENPOT_PUBLIC_URI=http://localhost:9001 - -## Feature flags. -PENPOT_FLAGS=enable-registration enable-login disable-email-verification - -## Temporal workaround because of bad builtin default - -PENPOT_HTTP_SERVER_HOST=0.0.0.0 - -## Standard database connection parameters (only postgresql is supported): - -PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot -PENPOT_DATABASE_USERNAME=penpot -PENPOT_DATABASE_PASSWORD=penpot - -## Redis is used for the websockets notifications. - -PENPOT_REDIS_URI=redis://penpot-redis/0 - -## By default, files uploaded by users are stored in local -## filesystem. But it can be configured to store in AWS S3. - -PENPOT_ASSETS_STORAGE_BACKEND=assets-fs -PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets - -## Telemetry. When enabled, a periodical process will send anonymous -## data about this instance. Telemetry data will enable us to learn on -## how the application is used, based on real scenarios. If you want -## to help us, please leave it enabled. - -PENPOT_TELEMETRY_ENABLED=true - -## Email sending configuration. By default, emails are printed in the -## console, but for production usage is recommended to setup a real -## SMTP provider. Emails are used to confirm user registrations. - -PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com -PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com -# PENPOT_SMTP_HOST= -# PENPOT_SMTP_PORT= -# PENPOT_SMTP_USERNAME= -# PENPOT_SMTP_PASSWORD= -# PENPOT_SMTP_TLS=true -# PENPOT_SMTP_SSL=false - -## Comma separated list of allowed domains to register. Empty to allow -## all. - -# PENPOT_REGISTRATION_DOMAIN_WHITELIST="" - -## Authentication providers - -## Google - -# PENPOT_GOOGLE_CLIENT_ID= -# PENPOT_GOOGLE_CLIENT_SECRET= - -## GitHub - -# PENPOT_GITHUB_CLIENT_ID= -# PENPOT_GITHUB_CLIENT_SECRET= - -## GitLab - -# PENPOT_GITLAB_BASE_URI=https://gitlab.com -# PENPOT_GITLAB_CLIENT_ID= -# PENPOT_GITLAB_CLIENT_SECRET= - -## OpenID Connect (since 1.5.0) - -# PENPOT_OIDC_BASE_URI= -# PENPOT_OIDC_CLIENT_ID= -# PENPOT_OIDC_CLIENT_SECRET= - -## LDAP -## -## NOTE: to enable ldap, you will need to put 'enable-login-with-ldap' -## on the 'PENPOT_FLAGS' environment variable. - -# PENPOT_LDAP_HOST=ldap -# PENPOT_LDAP_PORT=10389 -# PENPOT_LDAP_SSL=false -# PENPOT_LDAP_STARTTLS=false -# PENPOT_LDAP_BASE_DN=ou=people,dc=planetexpress,dc=com -# PENPOT_LDAP_BIND_DN=cn=admin,dc=planetexpress,dc=com -# PENPOT_LDAP_BIND_PASSWORD=GoodNewsEveryone -# PENPOT_LDAP_ATTRS_USERNAME=uid -# PENPOT_LDAP_ATTRS_EMAIL=mail -# PENPOT_LDAP_ATTRS_FULLNAME=cn diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index 5b3ad2a65..586f39873 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -5,20 +5,43 @@ networks: penpot: volumes: - penpot_postgres_data: - penpot_assets_data: + penpot_postgres_v15: + penpot_assets: + # penpot_traefik: + # penpot_minio: services: + ## Traefik service declaration example. Consider using it if you are + ## going to expose penpot to the internet or different host than + ## `localhost`. + + # traefik: + # image: traefik:v2.9 + # networks: + # - penpot + # command: + # - "--api.insecure=true" + # - "--entryPoints.web.address=:80" + # - "--providers.docker=true" + # - "--providers.docker.exposedbydefault=false" + # - "--entryPoints.websecure.address=:443" + # - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" + # - "--certificatesresolvers.letsencrypt.acme.email=" + # - "--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json" + # volumes: + # - "penpot_traefik:/traefik" + # - "/var/run/docker.sock:/var/run/docker.sock" + # ports: + # - "80:80" + # - "443:443" + penpot-frontend: image: "penpotapp/frontend:latest" ports: - 9001:80 volumes: - - penpot_assets_data:/opt/data - - env_file: - - config.env + - penpot_assets:/opt/data depends_on: - penpot-backend @@ -27,51 +50,250 @@ services: networks: - penpot + labels: + - "traefik.enable=true" + + ## HTTP: example of labels for the case if you are going to + ## expose penpot to the internet using only HTTP (without HTTPS) + ## with traefik + + # - "traefik.http.routers.penpot-http.entrypoints=web" + # - "traefik.http.routers.penpot-http.rule=Host(``)" + # - "traefik.http.services.penpot-http.loadbalancer.server.port=80" + + ## HTTPS: example of labels for the case if you are going to + ## expose penpot to the internet using with HTTPS using traefik + + # - "traefik.http.middlewares.http-redirect.redirectscheme.scheme=https" + # - "traefik.http.middlewares.http-redirect.redirectscheme.permanent=true" + # - "traefik.http.routers.penpot-http.entrypoints=web" + # - "traefik.http.routers.penpot-http.rule=Host(``)" + # - "traefik.http.routers.penpot-http.middlewares=http-redirect" + # - "traefik.http.routers.penpot-https.entrypoints=websecure" + # - "traefik.http.routers.penpot-https.rule=Host(``)" + # - "traefik.http.services.penpot-https.loadbalancer.server.port=80" + # - "traefik.http.routers.penpot-https.tls=true" + # - "traefik.http.routers.penpot-https.tls.certresolver=letsencrypt" + + ## Configuration envronment variables for frontend the + ## container. In this case this container only needs the + ## `PENPOT_FLAGS`. This environment variable is shared with other + ## services but not all flags are relevant to all services. + ## + ## Relevant flags for frontend: + ## - demo-users + ## - login-with-github + ## - login-with-gitlab + ## - login-with-google + ## - login-with-ldap + ## - login-with-oidc + ## - login-with-password + ## - registration + ## - webhooks + ## + ## You can read more about all available flags on: + ## https://help.penpot.app/technical-guide/configuration/#advanced-configuration + + environment: + - PENPOT_FLAGS=enable-registration enable-login-with-password + penpot-backend: image: "penpotapp/backend:latest" volumes: - - penpot_assets_data:/opt/data + - penpot_assets:/opt/data depends_on: - penpot-postgres - penpot-redis - env_file: - - config.env - networks: - penpot + ## Configuration envronment variables for backend the + ## container. + ## + ## Relevant flags for backend: + ## - demo-users + ## - email-verification + ## - log-emails + ## - log-invitation-tokens + ## - login-with-github + ## - login-with-gitlab + ## - login-with-google + ## - login-with-ldap + ## - login-with-oidc + ## - login-with-password + ## - registration + ## - secure-session-cookies + ## - smtp + ## - smtp-debug + ## - telemetry + ## - webhooks + ## + ## You can read more about all available flags and other + ## environment variables for the backend here: + ## https://help.penpot.app/technical-guide/configuration/#advanced-configuration + + environment: + - PENPOT_FLAGS=enable-registration enable-login disable-email-verification enable-smtp + + ## Setup initial administration user, uncommit only if you are + ## going to use the penpot-admin; Once uncommented, the special + ## user will be created on application start. This user can only + ## be used for access admin, you will not be able to login with + ## it on penpot application. + + # - PENPOT_SETUP_ADMIN_EMAIL=admin@example.com + # - PENPOT_SETUP_ADMIN_PASSWORD=password + + ## Public URI. If you are going to expose this instance to the + ## internet, or use it under different domain than 'localhost' + ## consider using traefik and set the + ## `disable-secure-session-cookies` if you are not going to + ## serve penpot under HTTPS. + + - PENPOT_PUBLIC_URI=http://localhost:9001 + + ## Database connection parameters. Don't touch them unless you + ## are using custom postgresql connection parameters + + - PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot + - PENPOT_DATABASE_USERNAME=penpot + - PENPOT_DATABASE_PASSWORD=penpot + + ## Redis is used for the websockets notifications. Don't touch + ## unless the redis container has different parameters or + ## different name. + + - PENPOT_REDIS_URI=redis://penpot-redis/0 + + ## Default configuration for assets storage: using filesystem + ## based with all files stored in a docker volume. + + - PENPOT_ASSETS_STORAGE_BACKEND=assets-fs + - PENPOT_STORAGE_ASSETS_FS_DIRECTORY=/opt/data/assets + + ## Also can be configured to to use a S3 compatible storage + ## service like MiniIO. Look below for minio service setup. + + # - AWS_ACCESS_KEY_ID= + # - AWS_SECRET_ACCESS_KEY= + # - PENPOT_ASSETS_STORAGE_BACKEND=assets-s3 + # - PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://penpot-minio:9000 + # - PENPOT_STORAGE_ASSETS_S3_BUCKET= + + ## Telemetry. When enabled, a periodical process will send + ## anonymous data about this instance. Telemetry data will + ## enable us to learn on how the application is used, based on + ## real scenarios. If you want to help us, please leave it + ## enabled. You can audit what data we send with the code + ## available on github + - PENPOT_TELEMETRY_ENABLED=true + + ## Example SMTP/Email configuration. By default, emails are sent + ## to the mailcatch service, but for production usage is + ## recommended to setup a real SMTP provider. Emails are used to + ## confirm user registrations & invitations. Look below how + ## mailcatch service is configured. + - PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com + - PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com + - PENPOT_SMTP_HOST=penpot-mailcatch + - PENPOT_SMTP_PORT=1025 + - PENPOT_SMTP_USERNAME= + - PENPOT_SMTP_PASSWORD= + - PENPOT_SMTP_TLS=false + - PENPOT_SMTP_SSL=false + penpot-exporter: image: "penpotapp/exporter:latest" - env_file: - - config.env + networks: + - penpot + environment: # Don't touch it; this uses internal docker network to # communicate with the frontend. - PENPOT_PUBLIC_URI=http://penpot-frontend - networks: - - penpot + + ## Redis is used for the websockets notifications. + - PENPOT_REDIS_URI=redis://penpot-redis/0 penpot-postgres: - image: "postgres:14" + image: "postgres:15" restart: always stop_signal: SIGINT + volumes: + - penpot_postgres_v15:/var/lib/postgresql/data + + networks: + - penpot + environment: - POSTGRES_INITDB_ARGS=--data-checksums - POSTGRES_DB=penpot - POSTGRES_USER=penpot - POSTGRES_PASSWORD=penpot - volumes: - - penpot_postgres_data:/var/lib/postgresql/data - - networks: - - penpot - penpot-redis: image: redis:7 restart: always networks: - penpot + + ## An optional admin application for pentpot. It allows manage + ## users, teams and inspect some parts of the database. You can read + ## more about it on: https://github.com/penpot/penpot-admin + + # penpot-admin: + # image: "penpotapp/admin:alpha" + # networks: + # - penpot + + # depends_on: + # - penpot-postgres + # - penpot-backend + + # environment: + # - PENPOT_PUBLIC_URI=http://localhost:9001 + # - PENPOT_API_URI=http://penpot-frontend/ + + # - PENPOT_DATABASE_HOST=penpot-postgres + # - PENPOT_DATABASE_NAME=penpot + # - PENPOT_DATABASE_USERNAME=penpot + # - PENPOT_DATABASE_PASSWORD=penpot + # - PENPOT_REDIS_URI=redis://penpot-redis/0 + # - PENPOT_DEBUG="false" + + ## A mailcatch service, used as temporal SMTP server. You can access + ## via HTTP to the port 1080 for read all emails the penpot platform + ## has sent. Should be only used as a temporal solution meanwhile + ## you don't have a real SMTP provider configured. + + penpot-mailcatch: + image: sj26/mailcatcher:latest + restart: always + expose: + - '1025' + ports: + - "1080:1080" + + ## Example configuration of MiniIO (S3 compatible object storage + ## service); If you don't have preference, then just use filesystem, + ## this is here just for the completeness. + + # minio: + # image: "minio/minio:latest" + # command: minio server /mnt/data --console-address ":9001" + # + # volumes: + # - "penpot_minio:/mnt/data" + # + # environment: + # - MINIO_ROOT_USER=minioadmin + # - MINIO_ROOT_PASSWORD=minioadmin + # + # ports: + # - 9000:9000 + # - 9001:9001 + + diff --git a/docker/images/files/nginx.conf b/docker/images/files/nginx.conf index 0302a2a83..2d2317135 100644 --- a/docker/images/files/nginx.conf +++ b/docker/images/files/nginx.conf @@ -25,7 +25,7 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; - error_log /dev/stdout; + error_log /dev/stderr; access_log /dev/stdout; gzip on; @@ -60,29 +60,6 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; etag off; - root /var/www/app/; - - location ~* \.(js|css).*$ { - add_header Cache-Control "max-age=86400" always; # 24 hours - } - - location ~* \.(html).*$ { - add_header Cache-Control "no-cache, max-age=0" always; - } - - location /api/export { - proxy_pass http://penpot-exporter:6061; - } - - location /api { - proxy_pass http://penpot-backend:6060/api; - } - - location /ws/notifications { - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_pass http://penpot-backend:6060/ws/notifications; - } location @handle_redirect { set $redirect_uri "$upstream_http_location"; @@ -116,5 +93,35 @@ http { alias /opt/data/assets; add_header x-internal-redirect "$upstream_http_x_accel_redirect"; } + + + location /api/export { + proxy_pass http://penpot-exporter:6061; + } + + location /api { + proxy_pass http://penpot-backend:6060/api; + } + + location /admin { + proxy_pass http://penpot-admin:6063/admin; + } + + location /ws/notifications { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_pass http://penpot-backend:6060/ws/notifications; + } + + location / { + location ~* \.(js|css).*$ { + add_header Cache-Control "max-age=86400" always; # 24 hours + } + + location ~* \.(html).*$ { + add_header Cache-Control "no-cache, max-age=0" always; + } + root /var/www/app/; + } } } diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs index 34b228436..2efc846ba 100644 --- a/exporter/src/app/core.cljs +++ b/exporter/src/app/core.cljs @@ -44,3 +44,6 @@ (proc/on "uncaughtException" (fn [cause] (js/console.error cause))) + +(proc/on "SIGTERM" (fn [] (proc/exit 0))) +(proc/on "SIGINT" (fn [] (proc/exit 0))) diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 33b5be8e6..583520c5f 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -102,6 +102,10 @@ (= :profile-blocked (:code cause))) (reset! error (tr "errors.profile-blocked")) + (and (= :restriction (:type cause)) + (= :admin-only-profile (:code cause))) + (reset! error (tr "errors.profile-blocked")) + (and (= :validation (:type cause)) (= :wrong-credentials (:code cause))) (reset! error (tr "errors.wrong-credentials")) @@ -167,7 +171,8 @@ :label (tr "auth.password")}]] [:div.buttons-stack - (when (contains? @cf/flags :login) + (when (or (contains? @cf/flags :login) + (contains? @cf/flags :login-with-password)) [:& fm/submit-button {:label (tr "auth.login-submit") :data-test "login-submit"}]) @@ -228,6 +233,7 @@ [:& login-buttons {:params params}] (when (or (contains? @cf/flags :login) + (contains? @cf/flags :login-with-password) (contains? @cf/flags :login-with-ldap)) [:span.separator [:span.line] @@ -235,6 +241,7 @@ [:span.line]])]) (when (or (contains? @cf/flags :login) + (contains? @cf/flags :login-with-password) (contains? @cf/flags :login-with-ldap)) [:& login-form {:params params :on-success-callback on-success-callback}])]) @@ -247,7 +254,8 @@ [:& login-methods {:params params}] [:div.links - (when (contains? @cf/flags :login) + (when (or (contains? @cf/flags :login) + (contains? @cf/flags :login-with-password)) [:div.link-entry [:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request)) :data-test "forgot-password"} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index da9dad2b1..e31c9dca9 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1441,7 +1441,7 @@ msgstr "Resend invitation" #: src/app/main/ui/dashboard/team.cljs msgid "labels.copy-invitation-link" -msgstr "Copy invitation link" +msgstr "Copy link" #: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ac1b9e734..032864afc 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1611,7 +1611,7 @@ msgstr "Reenviar invitacion" #: src/app/main/ui/dashboard/team.cljs msgid "labels.copy-invitation-link" -msgstr "Copiar link de invitación" +msgstr "Copiar enlace" #: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs msgid "labels.retry" diff --git a/manage.sh b/manage.sh index 647fbbc38..eae0538fd 100755 --- a/manage.sh +++ b/manage.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -set -ex export ORGANIZATION="penpotapp"; export DEVENV_IMGNAME="$ORGANIZATION/devenv"; @@ -11,6 +10,8 @@ export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD); export CURRENT_HASH=$(git rev-parse --short HEAD); export CURRENT_COMMITS=$(git rev-list --count HEAD) +set -ex + function print-current-version { echo -n "$CURRENT_VERSION-$CURRENT_COMMITS-g$CURRENT_HASH" } @@ -78,7 +79,7 @@ function log-devenv { docker compose -p $DEVENV_PNAME -f docker/devenv/docker-compose.yaml logs -f --tail=50 } -function run-devenv { +function run-devenv-tmux { if [[ ! $(docker ps -f "name=penpot-devenv-main" -q) ]]; then start-devenv fi @@ -86,6 +87,14 @@ function run-devenv { docker exec -ti penpot-devenv-main sudo -EH -u penpot /home/start-tmux.sh } +function run-devenv-shell { + if [[ ! $(docker ps -f "name=penpot-devenv-main" -q) ]]; then + start-devenv + fi + docker exec -ti penpot-devenv-main sudo -EH -u penpot bash +} + + function build { echo ">> build start: $1" local version=$(print-current-version); @@ -164,6 +173,20 @@ function build-exporter-bundle { echo ">> bundle exporter end"; } +function build-docker-images { + rsync -avr --delete ./bundles/frontend/ ./docker/images/bundle-frontend/; + rsync -avr --delete ./bundles/backend/ ./docker/images/bundle-backend/; + rsync -avr --delete ./bundles/exporter/ ./docker/images/bundle-exporter/; + + pushd ./docker/images; + + docker build -t penpotapp/frontend:$CURRENT_BRANCH -f Dockerfile.frontend .; + docker build -t penpotapp/backend:$CURRENT_BRANCH -f Dockerfile.backend .; + docker build -t penpotapp/exporter:$CURRENT_BRANCH -f Dockerfile.exporter .; + + popd; +} + function usage { echo "PENPOT build & release manager" echo "USAGE: $0 OPTION" @@ -203,7 +226,10 @@ case $1 in start-devenv ${@:2} ;; run-devenv) - run-devenv ${@:2} + run-devenv-tmux ${@:2} + ;; + run-devenv-shell) + run-devenv-shell ${@:2} ;; stop-devenv) stop-devenv ${@:2} @@ -216,6 +242,12 @@ case $1 in ;; # production builds + build-bundle) + build-frontend-bundle; + build-backend-bundle; + build-exporter-bundle; + ;; + build-frontend-bundle) build-frontend-bundle; ;; @@ -228,6 +260,10 @@ case $1 in build-exporter-bundle; ;; + build-docker-images) + build-docker-images + ;; + # Docker Image Tasks *) usage