Merge pull request #2686 from penpot/niwinz-enhancements-5

General enhancements & Bugfixes
This commit is contained in:
Alejandro 2022-12-27 11:58:22 +01:00 committed by GitHub
commit fe11b37b8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 1355 additions and 717 deletions

View file

@ -2,7 +2,7 @@
export PENPOT_HOST=devenv export PENPOT_HOST=devenv
export PENPOT_TENANT=dev 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_URI="postgresql://172.17.0.1:5432/penpot"
# export PENPOT_DATABASE_USERNAME="penpot" # export PENPOT_DATABASE_USERNAME="penpot"

View file

@ -2,7 +2,7 @@
export PENPOT_HOST=devenv export PENPOT_HOST=devenv
export PENPOT_TENANT=dev 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 set -ex

26
backend/src/app/auth.clj Normal file
View file

@ -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})))

View file

@ -106,6 +106,9 @@
(s/def ::file-change-snapshot-every ::us/integer) (s/def ::file-change-snapshot-every ::us/integer)
(s/def ::file-change-snapshot-timeout ::dt/duration) (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 ::default-executor-parallelism ::us/integer)
(s/def ::scheduled-executor-parallelism ::us/integer) (s/def ::scheduled-executor-parallelism ::us/integer)
@ -295,6 +298,9 @@
::srepl-host ::srepl-host
::srepl-port ::srepl-port
::setup-admin-email
::setup-admin-password
::assets-storage-backend ::assets-storage-backend
::storage-assets-fs-directory ::storage-assets-fs-directory
::storage-assets-s3-bucket ::storage-assets-s3-bucket

View file

@ -290,7 +290,11 @@
{:pool (ig/ref ::db/pool) {:pool (ig/ref ::db/pool)
:executor (ig/ref ::wrk/executor) :executor (ig/ref ::wrk/executor)
:storage (ig/ref ::sto/storage) :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 :app.http.websocket/handler
{:pool (ig/ref ::db/pool) {:pool (ig/ref ::db/pool)
@ -385,8 +389,8 @@
:max-age cf/deletion-delay} :max-age cf/deletion-delay}
:app.tasks.objects-gc/handler :app.tasks.objects-gc/handler
{:pool (ig/ref ::db/pool) {::db/pool (ig/ref ::db/pool)
:storage (ig/ref ::sto/storage)} ::sto/storage (ig/ref ::sto/storage)}
:app.tasks.file-gc/handler :app.tasks.file-gc/handler
{:pool (ig/ref ::db/pool)} {:pool (ig/ref ::db/pool)}
@ -403,6 +407,9 @@
{:port (cf/get :srepl-port) {:port (cf/get :srepl-port)
:host (cf/get :srepl-host)} :host (cf/get :srepl-host)}
:app.setup/initial-profile
{::db/pool (ig/ref ::db/pool)}
:app.setup/builtin-templates :app.setup/builtin-templates
{::http.client/client (ig/ref ::http.client/client)} {::http.client/client (ig/ref ::http.client/client)}

View file

@ -132,7 +132,7 @@
(defmethod run-collector! :counter (defmethod run-collector! :counter
[{:keys [::mdef/instance]} {:keys [inc labels] :or {inc 1 labels default-empty-labels}}] [{: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)))) (.inc ^Counter$Child instance (double inc))))
(defmethod run-collector! :gauge (defmethod run-collector! :gauge

View file

@ -271,6 +271,37 @@
{:name "0087-mod-task-table" {:name "0087-mod-task-table"
:fn (mg/resource "app/migrations/sql/0087-mod-task-table.sql")} :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")}
]) ])

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -0,0 +1 @@
DROP TABLE file_share_token;

View file

@ -0,0 +1 @@
DROP TABLE profile_attr;

View file

@ -0,0 +1 @@
DROP TABLE storage_data;

View file

@ -0,0 +1 @@
DROP TABLE storage_pending;

View file

@ -0,0 +1,2 @@
ALTER TABLE profile
ADD COLUMN is_admin boolean DEFAULT false;

View file

@ -35,6 +35,8 @@
[yetti.request :as yrq] [yetti.request :as yrq]
[yetti.response :as yrs])) [yetti.response :as yrs]))
(s/def ::profile-id ::us/uuid)
(defn- default-handler (defn- default-handler
[_] [_]
(p/rejected (ex/error :type :not-found))) (p/rejected (ex/error :type :not-found)))
@ -72,8 +74,11 @@
(let [type (keyword (:type params)) (let [type (keyword (:type params))
data (into {::http/request request} params) data (into {::http/request request} params)
data (if profile-id data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id) (assoc data
(dissoc data :profile-id)) :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 (get methods type default-handler)]
(-> (method data) (-> (method data)
@ -90,8 +95,11 @@
(let [type (keyword (:type params)) (let [type (keyword (:type params))
data (into {::http/request request} params) data (into {::http/request request} params)
data (if profile-id data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id) (assoc data
(dissoc data :profile-id)) :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 (get methods type default-handler)]
(-> (method data) (-> (method data)
@ -109,9 +117,8 @@
etag (yrq/get-header request "if-none-match") etag (yrq/get-header request "if-none-match")
data (into {::http/request request ::cond/key etag} params) data (into {::http/request request ::cond/key etag} params)
data (if profile-id data (if profile-id
(assoc data :profile-id profile-id ::session-id session-id) (assoc data ::profile-id profile-id ::session-id session-id)
(dissoc data :profile-id)) (dissoc data ::profile-id))
method (get methods cmd default-handler)] method (get methods cmd default-handler)]
(binding [cond/*enabled* true] (binding [cond/*enabled* true]
(-> (method data) (-> (method data)
@ -152,9 +159,12 @@
(letfn [(handle-audit [params result] (letfn [(handle-audit [params result]
(let [resultm (meta result) (let [resultm (meta result)
request (::http/request params) request (::http/request params)
profile-id (or (::audit/profile-id resultm) profile-id (or (::audit/profile-id resultm)
(:profile-id result) (:profile-id result)
(:profile-id params) (if (= (::type cfg) "command")
(::profile-id params)
(:profile-id params))
uuid/zero) uuid/zero)
props (-> (or (::audit/replace-props resultm) props (-> (or (::audit/replace-props resultm)
@ -209,21 +219,24 @@
(wrap-audit cfg $ mdata)) (wrap-audit cfg $ mdata))
spec (or (::sv/spec mdata) (s/spec any?)) 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)) (l/debug :hint "register method" :name (::sv/name mdata))
(with-meta (with-meta
(fn [params] (fn [params]
;; Raise authentication error when rpc method requires auth but ;; Raise authentication error when rpc method requires auth but
;; no profile-id is found in the request. ;; no profile-id is found in the request.
(let [profile-id (if (= "command" (::type cfg))
(::profile-id params)
(:profile-id params))]
(p/do! (p/do!
(if (and auth? (not (uuid? (:profile-id params)))) (if (and auth? (not (uuid? profile-id)))
(ex/raise :type :authentication (ex/raise :type :authentication
:code :authentication-required :code :authentication-required
:hint "authentication required for this endpoint") :hint "authentication required for this endpoint")
(let [params (us/conform spec params)] (let [params (us/conform spec params)]
(f cfg params))))) (f cfg params))))))
mdata))) mdata)))
(defn- process-method (defn- process-method
@ -262,6 +275,7 @@
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)] (let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
(->> (sv/scan-ns 'app.rpc.commands.binfile (->> (sv/scan-ns 'app.rpc.commands.binfile
'app.rpc.commands.comments 'app.rpc.commands.comments
'app.rpc.commands.profile
'app.rpc.commands.management 'app.rpc.commands.management
'app.rpc.commands.verify-token 'app.rpc.commands.verify-token
'app.rpc.commands.search 'app.rpc.commands.search

View file

@ -15,6 +15,7 @@
[app.db :as db] [app.db :as db]
[app.http :as-alias http] [app.http :as-alias http]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit] [app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
@ -41,7 +42,7 @@
:profile-id :ip-addr :props :context]) :profile-id :ip-addr :props :context])
(defn- handle-events (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) (let [ip-addr (audit/parse-client-ip request)
xform (comp xform (comp
(map #(assoc % :profile-id profile-id)) (map #(assoc % :profile-id profile-id))
@ -53,7 +54,6 @@
(when (seq events) (when (seq events)
(db/insert-multi! pool :audit-log event-columns events)))) (db/insert-multi! pool :audit-log event-columns events))))
(s/def ::profile-id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::type ::us/string) (s/def ::type ::us/string)
(s/def ::props (s/map-of ::us/keyword any?)) (s/def ::props (s/map-of ::us/keyword any?))
@ -67,7 +67,8 @@
(s/def ::events (s/every ::event)) (s/def ::events (s/every ::event))
(s/def ::push-audit-events (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 (sv/defmethod ::push-audit-events
{::climit/queue :push-audit-events {::climit/queue :push-audit-events

View file

@ -6,6 +6,7 @@
(ns app.rpc.commands.auth (ns app.rpc.commands.auth
(:require (:require
[app.auth :as auth]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
@ -15,6 +16,8 @@
[app.emails :as eml] [app.emails :as eml]
[app.http.session :as session] [app.http.session :as session]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit] [app.rpc.climit :as climit]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@ -23,7 +26,6 @@
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -31,7 +33,6 @@
(s/def ::fullname ::us/not-empty-string) (s/def ::fullname ::us/not-empty-string)
(s/def ::lang ::us/string) (s/def ::lang ::us/string)
(s/def ::path ::us/string) (s/def ::path ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::password ::us/not-empty-string) (s/def ::password ::us/not-empty-string)
(s/def ::old-password ::us/not-empty-string) (s/def ::old-password ::us/not-empty-string)
(s/def ::theme ::us/string) (s/def ::theme ::us/string)
@ -40,22 +41,6 @@
;; ---- HELPERS ;; ---- 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? (defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if "Returns true if email's domain is in the given whitelist or if
given whitelist is an empty string." given whitelist is an empty string."
@ -84,9 +69,10 @@
;; ---- COMMAND: login with password ;; ---- COMMAND: login with password
(defn 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 (ex/raise :type :restriction
:code :login-disabled :code :login-disabled
:hint "login is disabled in this instance")) :hint "login is disabled in this instance"))
@ -96,7 +82,7 @@
(ex/raise :type :validation (ex/raise :type :validation
:code :account-without-password :code :account-without-password
:hint "the current account does not have 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] (validate-profile [profile]
(when-not profile (when-not profile
@ -126,27 +112,37 @@
(profile/decode-profile-row)) (profile/decode-profile-row))
invitation (when-let [token (:invitation-token params)] 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 ;; 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 ;; invitation because invitations matches exactly; and user can't login with other email and
;; accept invitation with other email ;; accept invitation with other email
response (if (and (some? invitation) (= (:id profile) (:member-id invitation))) response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
{:invitation-token (:invitation-token params)} {: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 (-> response
(rph/with-transform (session/create-fn session (:id profile))) (rph/with-transform (session/create-fn session (:id profile)))
(rph/with-meta {::audit/props (audit/profile->props profile) (rph/with-meta {::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))) ::audit/profile-id (:id profile)}))))))
(s/def ::scope ::us/string)
(s/def ::login-with-password (s/def ::login-with-password
(s/keys :req-un [::email ::password] (s/keys :req-un [::email ::password]
:opt-un [::invitation-token])) :opt-un [::invitation-token ::scope]))
(sv/defmethod ::login-with-password (sv/defmethod ::login-with-password
"Performs authentication using penpot password." "Performs authentication using penpot password."
{:auth false {::rpc/auth false
::climit/queue :auth ::climit/queue :auth
::doc/added "1.15"} ::doc/added "1.15"}
[cfg params] [cfg params]
@ -155,11 +151,11 @@
;; ---- COMMAND: Logout ;; ---- COMMAND: Logout
(s/def ::logout (s/def ::logout
(s/keys :opt-un [::profile-id])) (s/keys :opt [::rpc/profile-id]))
(sv/defmethod ::logout (sv/defmethod ::logout
"Clears the authentication cookie and logout the current session." "Clears the authentication cookie and logout the current session."
{:auth false {::rpc/auth false
::doc/added "1.15"} ::doc/added "1.15"}
[{:keys [session] :as cfg} _] [{:keys [session] :as cfg} _]
(rph/with-transform {} (session/delete-fn session))) (rph/with-transform {} (session/delete-fn session)))
@ -167,13 +163,13 @@
;; ---- COMMAND: Recover Profile ;; ---- COMMAND: Recover Profile
(defn 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] (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))) (:profile-id tdata)))
(update-password [conn profile-id] (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/update! conn :profile {:password pwd} {:id profile-id})))]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
@ -186,7 +182,7 @@
(s/keys :req-un [::token ::password])) (s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile (sv/defmethod ::recover-profile
{:auth false {::rpc/auth false
::climit/queue :auth ::climit/queue :auth
::doc/added "1.15"} ::doc/added "1.15"}
[cfg params] [cfg params]
@ -195,13 +191,13 @@
;; ---- COMMAND: Prepare Register ;; ---- COMMAND: Prepare Register
(defn validate-register-attempt! (defn validate-register-attempt!
[{:keys [pool sprops]} params] [{:keys [::db/pool] :as cfg} params]
(when-not (contains? cf/flags :registration) (when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token) (if-not (contains? params :invitation-token)
(ex/raise :type :restriction (ex/raise :type :restriction
:code :registration-disabled) :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)) (when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction (ex/raise :type :restriction
:code :email-does-not-match-invitation :code :email-does-not-match-invitation
@ -235,7 +231,7 @@
(pos? (compare elapsed register-retry-threshold)))) (pos? (compare elapsed register-retry-threshold))))
(defn prepare-register (defn prepare-register
[{:keys [pool sprops] :as cfg} params] [{:keys [::db/pool] :as cfg} params]
(validate-register-attempt! cfg params) (validate-register-attempt! cfg params)
@ -264,7 +260,7 @@
params (d/without-nils params) params (d/without-nils params)
token (tokens/generate sprops params)] token (tokens/generate (::main/props cfg) params)]
(with-meta {:token token} (with-meta {:token token}
{::audit/profile-id uuid/zero}))) {::audit/profile-id uuid/zero})))
@ -273,7 +269,7 @@
:opt-un [::invitation-token])) :opt-un [::invitation-token]))
(sv/defmethod ::prepare-register-profile (sv/defmethod ::prepare-register-profile
{:auth false {::rpc/auth false
::doc/added "1.15"} ::doc/added "1.15"}
[cfg params] [cfg params]
(prepare-register cfg params)) (prepare-register cfg params))
@ -293,7 +289,7 @@
(db/tjson)) (db/tjson))
password (if-let [password (:password params)] password (if-let [password (:password params)]
(derive-password password) (auth/derive-password password)
"!") "!")
locale (:locale params) locale (:locale params)
@ -339,15 +335,15 @@
(assoc :default-project-id (:default-project-id team))))) (assoc :default-project-id (:default-project-id team)))))
(defn send-email-verification! (defn send-email-verification!
[conn sprops profile] [conn props profile]
(let [vtoken (tokens/generate sprops (let [vtoken (tokens/generate props
{:iss :verify-email {:iss :verify-email
:exp (dt/in-future "72h") :exp (dt/in-future "72h")
:profile-id (:id profile) :profile-id (:id profile)
:email (:email profile)}) :email (:email profile)})
;; NOTE: this token is mainly used for possible complains ;; NOTE: this token is mainly used for possible complains
;; identification on the sns webhook ;; identification on the sns webhook
ptoken (tokens/generate sprops ptoken (tokens/generate props
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile) :profile-id (:id profile)
:exp (dt/in-future {:days 30})})] :exp (dt/in-future {:days 30})})]
@ -360,8 +356,8 @@
:extra-data ptoken}))) :extra-data ptoken})))
(defn register-profile (defn register-profile
[{:keys [conn sprops session] :as cfg} {:keys [token] :as params}] [{:keys [conn session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify sprops {:token token :iss :prepared-register}) (let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
params (merge params claims) params (merge params claims)
is-active (or (:is-active params) is-active (or (:is-active params)
@ -377,7 +373,7 @@
(create-profile-relations conn) (create-profile-relations conn)
(profile/decode-profile-row))) (profile/decode-profile-row)))
invitation (when-let [token (:invitation-token params)] 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 ;; If profile is filled in claims, means it tries to register
;; again, so we proceed to update the modified-at attr ;; again, so we proceed to update the modified-at attr
@ -399,7 +395,7 @@
;; email. ;; email.
(and (some? invitation) (= (:email profile) (:member-email invitation))) (and (some? invitation) (= (:email profile) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile)) (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 {:invitation-token token}]
(-> resp (-> resp
(rph/with-transform (session/create-fn session (:id profile))) (rph/with-transform (session/create-fn session (:id profile)))
@ -426,7 +422,7 @@
;; In all other cases, send a verification email. ;; In all other cases, send a verification email.
:else :else
(do (do
(send-email-verification! conn sprops profile) (send-email-verification! conn (::main/props cfg) profile)
(rph/with-meta profile (rph/with-meta profile
{::audit/replace-props (audit/profile->props profile) {::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))) ::audit/profile-id (:id profile)})))))
@ -435,10 +431,10 @@
(s/keys :req-un [::token ::fullname])) (s/keys :req-un [::token ::fullname]))
(sv/defmethod ::register-profile (sv/defmethod ::register-profile
{:auth false {::rpc/auth false
::climit/queue :auth ::climit/queue :auth
::doc/added "1.15"} ::doc/added "1.15"}
[{:keys [pool] :as cfg} params] [{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(-> (assoc cfg :conn conn) (-> (assoc cfg :conn conn)
(register-profile params)))) (register-profile params))))
@ -446,16 +442,16 @@
;; ---- COMMAND: Request Profile Recovery ;; ---- COMMAND: Request Profile Recovery
(defn 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}] (letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens/generate sprops (let [token (tokens/generate (::main/props cfg)
{:iss :password-recovery {:iss :password-recovery
:exp (dt/in-future "15m") :exp (dt/in-future "15m")
:profile-id id})] :profile-id id})]
(assoc profile :token token))) (assoc profile :token token)))
(send-email-notification [conn profile] (send-email-notification [conn profile]
(let [ptoken (tokens/generate sprops (let [ptoken (tokens/generate (::main/props cfg)
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile) :profile-id (:id profile)
:exp (dt/in-future {:days 30})})] :exp (dt/in-future {:days 30})})]
@ -493,7 +489,7 @@
(s/keys :req-un [::email])) (s/keys :req-un [::email]))
(sv/defmethod ::request-profile-recovery (sv/defmethod ::request-profile-recovery
{:auth false {::rpc/auth false
::doc/added "1.15"} ::doc/added "1.15"}
[cfg params] [cfg params]
(request-profile-recovery cfg params)) (request-profile-recovery cfg params))

View file

@ -15,10 +15,13 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.media :as media] [app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.projects :as projects] [app.rpc.queries.projects :as projects]
[app.storage :as sto] [app.storage :as sto]
[app.storage.tmp :as tmp] [app.storage.tmp :as tmp]
@ -291,7 +294,7 @@
(defn- retrieve-file (defn- retrieve-file
[pool file-id] [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)] (binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
(some-> (db/get* conn :file {:id file-id}) (some-> (db/get* conn :file {:id file-id})
(files/decode-row) (files/decode-row)
@ -864,18 +867,17 @@
;; --- Command: export-binfile ;; --- Command: export-binfile
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::include-libraries? ::us/boolean) (s/def ::include-libraries? ::us/boolean)
(s/def ::embed-assets? ::us/boolean) (s/def ::embed-assets? ::us/boolean)
(s/def ::export-binfile (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 (sv/defmethod ::export-binfile
"Export a penpot file in a binary format." "Export a penpot file in a binary format."
{::doc/added "1.15" {::doc/added "1.15"
::webhooks/event? true} ::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) (files/check-read-permissions! pool profile-id file-id)
(let [body (reify yrs/StreamableResponseBody (let [body (reify yrs/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream] (-write-body-to-stream [_ _ output-stream]
@ -890,16 +892,18 @@
(s/def ::file ::media/upload) (s/def ::file ::media/upload)
(s/def ::import-binfile (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 (sv/defmethod ::import-binfile
"Import a penpot file in a binary format." "Import a penpot file in a binary format."
{::doc/added "1.15" {::doc/added "1.15"
::webhooks/event? true} ::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] (db/with-atomic [conn pool]
(projects/check-read-permissions! conn profile-id project-id) (projects/check-read-permissions! conn profile-id project-id)
(import! (assoc cfg (let [ids (import! (assoc cfg
::input (:path file) ::input (:path file)
::project-id project-id ::project-id project-id
::ignore-index-errors? true)))) ::ignore-index-errors? true))]
(rph/with-meta ids
{::audit/props {:file nil :file-ids ids}}))))

View file

@ -12,6 +12,7 @@
[app.db :as db] [app.db :as db]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@ -41,7 +42,7 @@
(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-comment-threads (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]) :opt-un [::file-id ::share-id ::team-id])
#(or (:file-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)") window w as (partition by c.thread_id order by c.created_at asc)")
(defn retrieve-comment-threads (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) (files/check-comment-permissions! conn profile-id file-id share-id)
(->> (db/exec! conn [sql:comment-threads profile-id file-id]) (->> (db/exec! conn [sql:comment-threads profile-id file-id])
(into [] (map decode-row)))) (into [] (map decode-row))))
@ -85,11 +86,12 @@
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::get-unread-comment-threads (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 (sv/defmethod ::get-unread-comment-threads
{::doc/added "1.15"} {::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)] (with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(retrieve-unread-comment-threads conn params))) (retrieve-unread-comment-threads conn params)))
@ -122,7 +124,7 @@
"select * from threads where count_unread_comments > 0")) "select * from threads where count_unread_comments > 0"))
(defn retrieve-unread-comment-threads (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]) (->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
(into [] (map decode-row)))) (into [] (map decode-row))))
@ -132,12 +134,13 @@
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-comment-thread (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])) :opt-un [::share-id]))
(sv/defmethod ::get-comment-thread (sv/defmethod ::get-comment-thread
{::doc/added "1.15"} {::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)] (with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id) (files/check-comment-permissions! conn profile-id file-id share-id)
(let [sql (str "with threads as (" sql:comment-threads ")" (let [sql (str "with threads as (" sql:comment-threads ")"
@ -146,7 +149,7 @@
(decode-row))))) (decode-row)))))
(defn get-comment-thread (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 ")" (let [sql (str "with threads as (" sql:comment-threads ")"
"select * from threads where id = ?")] "select * from threads where id = ?")]
(-> (db/exec-one! conn [sql profile-id file-id 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 ::share-id (s/nilable ::us/uuid))
(s/def ::thread-id ::us/uuid) (s/def ::thread-id ::us/uuid)
(s/def ::get-comments (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])) :opt-un [::share-id]))
(sv/defmethod ::get-comments (sv/defmethod ::get-comments
{::doc/added "1.15"} {::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)] (with-open [conn (db/open pool)]
(let [thread (db/get-by-id conn :comment-thread thread-id)] (let [thread (db/get-by-id conn :comment-thread thread-id)]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-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 ::share-id (s/nilable ::us/uuid))
(s/def ::get-profiles-for-file-comments (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])) :opt-un [::share-id]))
(sv/defmethod ::get-profiles-for-file-comments (sv/defmethod ::get-profiles-for-file-comments
@ -199,7 +204,7 @@
participants on comment threads of the file." participants on comment threads of the file."
{::doc/added "1.15" {::doc/added "1.15"
::doc/changes ["1.15" "Imported from queries and renamed."]} ::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)] (with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id) (files/check-comment-permissions! conn profile-id file-id share-id)
(get-file-comments-users conn file-id profile-id))) (get-file-comments-users conn file-id profile-id)))
@ -240,19 +245,19 @@
(s/def ::page-id ::us/uuid) (s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::share-id (s/nilable ::us/uuid))
(s/def ::profile-id ::us/uuid)
(s/def ::position ::gpt/point) (s/def ::position ::gpt/point)
(s/def ::content ::us/string) (s/def ::content ::us/string)
(s/def ::frame-id ::us/uuid) (s/def ::frame-id ::us/uuid)
(s/def ::create-comment-thread (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])) :opt-un [::share-id]))
(sv/defmethod ::create-comment-thread (sv/defmethod ::create-comment-thread
{::doc/added "1.15" {::doc/added "1.15"
::webhooks/event? true} ::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] (db/with-atomic [conn pool]
(files/check-comment-permissions! conn profile-id file-id share-id) (files/check-comment-permissions! conn profile-id file-id share-id)
@ -268,7 +273,7 @@
(:next-seqn res))) (:next-seqn res)))
(defn create-comment-thread (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) (let [seqn (retrieve-next-seqn conn file-id)
now (dt/now) now (dt/now)
pname (retrieve-page-name conn params) pname (retrieve-page-name conn params)
@ -316,12 +321,13 @@
(s/def ::share-id (s/nilable ::us/uuid)) (s/def ::share-id (s/nilable ::us/uuid))
(s/def ::update-comment-thread-status (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])) :opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-status (sv/defmethod ::update-comment-thread-status
{::doc/added "1.15"} {::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] (db/with-atomic [conn pool]
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})] (let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not cthr (when-not cthr
@ -346,12 +352,13 @@
(s/def ::is-resolved ::us/boolean) (s/def ::is-resolved ::us/boolean)
(s/def ::update-comment-thread (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])) :opt-un [::share-id]))
(sv/defmethod ::update-comment-thread (sv/defmethod ::update-comment-thread
{::doc/added "1.15"} {::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] (db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})] (let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not thread (when-not thread
@ -370,7 +377,8 @@
(declare create-comment) (declare create-comment)
(s/def ::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])) :opt-un [::share-id]))
(sv/defmethod ::create-comment (sv/defmethod ::create-comment
@ -381,7 +389,7 @@
(create-comment conn params))) (create-comment conn params)))
(defn create-comment (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}) (let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
(decode-row)) (decode-row))
pname (retrieve-page-name conn thread)] pname (retrieve-page-name conn thread)]
@ -437,7 +445,8 @@
(declare update-comment) (declare update-comment)
(s/def ::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])) :opt-un [::share-id]))
(sv/defmethod ::update-comment (sv/defmethod ::update-comment
@ -447,7 +456,7 @@
(update-comment conn params))) (update-comment conn params)))
(defn update-comment (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}) (let [comment (db/get-by-id conn :comment id {:for-update true})
_ (when-not comment (ex/raise :type :not-found)) _ (when-not comment (ex/raise :type :not-found))
thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true}) thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
@ -476,11 +485,12 @@
;; --- COMMAND: Delete Comment Thread ;; --- COMMAND: Delete Comment Thread
(s/def ::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 (sv/defmethod ::delete-comment-thread
{::doc/added "1.15"} {::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] (db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})] (let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not (= (:owner-id thread) profile-id) (when-not (= (:owner-id thread) profile-id)
@ -493,11 +503,12 @@
;; --- COMMAND: Delete comment ;; --- COMMAND: Delete comment
(s/def ::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 (sv/defmethod ::delete-comment
{::doc/added "1.15"} {::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] (db/with-atomic [conn pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})] (let [comment (db/get-by-id conn :comment id {:for-update true})]
(when-not (= (:owner-id comment) profile-id) (when-not (= (:owner-id comment) profile-id)
@ -509,12 +520,13 @@
;; --- COMMAND: Update comment thread position ;; --- COMMAND: Update comment thread position
(s/def ::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])) :opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-position (sv/defmethod ::update-comment-thread-position
{::doc/added "1.15"} {::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] (db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})] (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) (files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
@ -528,12 +540,13 @@
;; --- COMMAND: Update comment frame ;; --- COMMAND: Update comment frame
(s/def ::update-comment-thread-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])) :opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-frame (sv/defmethod ::update-comment-thread-frame
{::doc/added "1.15"} {::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] (db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})] (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) (files/check-comment-permissions! conn profile-id (:file-id thread) share-id)

View file

@ -12,6 +12,7 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as cmd.auth] [app.rpc.commands.auth :as cmd.auth]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]
@ -26,7 +27,7 @@
"A command that is responsible of creating a demo purpose "A command that is responsible of creating a demo purpose
profile. It only works if the `demo-users` flag is enabled in the profile. It only works if the `demo-users` flag is enabled in the
configuration." configuration."
{:auth false {::rpc/auth false
::doc/added "1.15" ::doc/added "1.15"
::doc/changes ["1.15" "This method is migrated from mutations to commands."]} ::doc/changes ["1.15" "This method is migrated from mutations to commands."]}
[{:keys [pool] :as cfg} _] [{:keys [pool] :as cfg} _]

View file

@ -17,7 +17,9 @@
[app.common.types.shape-tree :as ctt] [app.common.types.shape-tree :as ctt]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.files.thumbnails :as-alias thumbs] [app.rpc.commands.files.thumbnails :as-alias thumbs]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond] [app.rpc.cond :as-alias cond]
@ -51,7 +53,6 @@
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::is-shared ::us/boolean) (s/def ::is-shared ::us/boolean)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::project-id ::us/uuid) (s/def ::project-id ::us/uuid)
(s/def ::search-term ::us/string) (s/def ::search-term ::us/string)
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
@ -256,7 +257,8 @@
(str (dt/format-instant modified-at :iso) "-" revn)) (str (dt/format-instant modified-at :iso) "-" revn))
(s/def ::get-file (s/def ::get-file
(s/keys :req-un [::profile-id ::id] (s/keys :req [::rpc/profile-id]
:req-un [::id]
:opt-un [::features])) :opt-un [::features]))
(sv/defmethod ::get-file (sv/defmethod ::get-file
@ -264,7 +266,7 @@
{::doc/added "1.17" {::doc/added "1.17"
::cond/get-object #(get-minimal-file %1 (:id %2)) ::cond/get-object #(get-minimal-file %1 (:id %2))
::cond/key-fn get-file-etag} ::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)] (with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id id)] (let [perms (get-permissions conn profile-id id)]
(check-read-permissions! perms) (check-read-permissions! perms)
@ -285,13 +287,14 @@
(s/def ::get-file-fragment (s/def ::get-file-fragment
(s/keys :req-un [::file-id ::fragment-id] (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 (sv/defmethod ::get-file-fragment
"Retrieve a file by its ID. Only authenticated users." "Retrieve a file by its ID. Only authenticated users."
{::doc/added "1.17" {::doc/added "1.17"
:auth false} ::rpc/:auth false}
[{:keys [pool] :as cfg} {:keys [profile-id file-id fragment-id share-id] :as params}] [{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id] :as params}]
(with-open [conn (db/open pool)] (with-open [conn (db/open pool)]
(let [perms (get-permissions conn profile-id file-id share-id)] (let [perms (get-permissions conn profile-id file-id share-id)]
(check-read-permissions! perms) (check-read-permissions! perms)
@ -319,7 +322,7 @@
(d/index-by :object-id :data))))) (d/index-by :object-id :data)))))
(s/def ::get-file-object-thumbnails (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 (sv/defmethod ::get-file-object-thumbnails
"Retrieve a file object thumbnails." "Retrieve a file object thumbnails."
@ -327,7 +330,7 @@
::cond/get-object #(get-minimal-file %1 (:file-id %2)) ::cond/get-object #(get-minimal-file %1 (:file-id %2))
::cond/reuse-key? true ::cond/reuse-key? true
::cond/key-fn get-file-etag} ::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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(get-object-thumbnails conn file-id))) (get-object-thumbnails conn file-id)))
@ -349,7 +352,7 @@
order by f.modified_at desc") order by f.modified_at desc")
(s/def ::get-project-files (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 (defn get-project-files
[conn project-id] [conn project-id]
@ -358,7 +361,7 @@
(sv/defmethod ::get-project-files (sv/defmethod ::get-project-files
"Get all files for the specified project." "Get all files for the specified project."
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(projects/check-read-permissions! conn profile-id project-id) (projects/check-read-permissions! conn profile-id project-id)
(get-project-files conn project-id))) (get-project-files conn project-id)))
@ -369,15 +372,14 @@
(declare get-has-file-libraries) (declare get-has-file-libraries)
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::has-file-libraries (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 (sv/defmethod ::has-file-libraries
"Checks if the file has libraries. Returns a boolean" "Checks if the file has libraries. Returns a boolean"
{::doc/added "1.15.1"} {::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)] (with-open [conn (db/open pool)]
(check-read-permissions! pool profile-id file-id) (check-read-permissions! pool profile-id file-id)
(get-has-file-libraries conn params))) (get-has-file-libraries conn params)))
@ -425,7 +427,8 @@
(s/def ::object-id ::us/uuid) (s/def ::object-id ::us/uuid)
(s/def ::get-page (s/def ::get-page
(s/and (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]) :opt-un [::page-id ::object-id ::features])
(fn [obj] (fn [obj]
(if (contains? obj :object-id) (if (contains? obj :object-id)
@ -443,7 +446,7 @@
Mainly used for rendering purposes." Mainly used for rendering purposes."
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(get-page conn params))) (get-page conn params)))
@ -492,7 +495,7 @@
(into #{} xform (db/exec! conn [sql:team-shared-files team-id])))) (into #{} xform (db/exec! conn [sql:team-shared-files team-id]))))
(s/def ::get-team-shared-files (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 (sv/defmethod ::get-team-shared-files
"Get all file (libraries) for the specified team." "Get all file (libraries) for the specified team."
@ -541,13 +544,14 @@
(handle-file-features client-features))))))) (handle-file-features client-features)))))))
(s/def ::get-file-libraries (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])) :opt-un [::features]))
(sv/defmethod ::get-file-libraries (sv/defmethod ::get-file-libraries
"Get libraries used by the specified file." "Get libraries used by the specified file."
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(get-file-libraries conn file-id features))) (get-file-libraries conn file-id features)))
@ -568,12 +572,12 @@
(db/exec! conn [sql:library-using-files file-id])) (db/exec! conn [sql:library-using-files file-id]))
(s/def ::get-library-file-references (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 (sv/defmethod ::get-library-file-references
"Returns all the file references that use specified file (library) id." "Returns all the file references that use specified file (library) id."
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(get-library-file-references conn file-id))) (get-library-file-references conn file-id)))
@ -606,11 +610,12 @@
(db/exec! conn [sql:team-recent-files team-id])) (db/exec! conn [sql:team-recent-files team-id]))
(s/def ::get-team-recent-files (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 (sv/defmethod ::get-team-recent-files
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(get-team-recent-files conn team-id))) (get-team-recent-files conn team-id)))
@ -638,12 +643,13 @@
(s/def ::revn ::us/integer) (s/def ::revn ::us/integer)
(s/def ::get-file-thumbnail (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])) :opt-un [::revn]))
(sv/defmethod ::get-file-thumbnail (sv/defmethod ::get-file-thumbnail
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(-> (get-file-thumbnail conn file-id revn) (-> (get-file-thumbnail conn file-id revn)
@ -729,14 +735,15 @@
(update :objects assoc-thumbnails page-id thumbs))))) (update :objects assoc-thumbnails page-id thumbs)))))
(s/def ::get-file-data-for-thumbnail (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])) :opt-un [::features]))
(sv/defmethod ::get-file-data-for-thumbnail (sv/defmethod ::get-file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used "Retrieves the data for generate the thumbnail of the file. Used
mainly for render thumbnails on dashboard." mainly for render thumbnails on dashboard."
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id file-id) (check-read-permissions! conn profile-id file-id)
(let [file (get-file conn file-id features)] (let [file (get-file conn file-id features)]
@ -753,23 +760,27 @@
(defn rename-file (defn rename-file
[conn {:keys [id name] :as params}] [conn {:keys [id name] :as params}]
(-> (db/update! conn :file (db/update! conn :file
{:name name {:name name
:modified-at (dt/now)} :modified-at (dt/now)}
{:id id}) {:id id}))
(select-keys [:id :name :created-at :modified-at])))
(s/def ::rename-file (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 (sv/defmethod ::rename-file
{::doc/added "1.17" {::doc/added "1.17"
::webhooks/event? true} ::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] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id) (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 ;; --- MUTATION COMMAND: set-file-shared
@ -779,10 +790,9 @@
(defn set-file-shared (defn set-file-shared
[conn {:keys [id is-shared] :as params}] [conn {:keys [id is-shared] :as params}]
(-> (db/update! conn :file (db/update! conn :file
{:is-shared is-shared} {:is-shared is-shared}
{:id id}) {:id id}))
(select-keys [:id :name :is-shared])))
(defn absorb-library (defn absorb-library
"Find all files using a shared library, and absorb all library assets "Find all files using a shared library, and absorb all library assets
@ -805,19 +815,25 @@
{:id id}))))))))) {:id id})))))))))
(s/def ::set-file-shared (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 (sv/defmethod ::set-file-shared
{::doc/added "1.17" {::doc/added "1.17"
::webhooks/event? true} ::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] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id) (check-edition-permissions! conn profile-id id)
(when-not is-shared (when-not is-shared
(absorb-library conn params) (absorb-library conn params)
(unlink-files 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 ;; --- MUTATION COMMAND: delete-file
@ -825,20 +841,26 @@
[conn {:keys [id] :as params}] [conn {:keys [id] :as params}]
(db/update! conn :file (db/update! conn :file
{:deleted-at (dt/now)} {:deleted-at (dt/now)}
{:id id}) {:id id}))
nil)
(s/def ::delete-file (s/def ::delete-file
(s/keys :req-un [::id ::profile-id])) (s/keys :req [::rpc/profile-id]
:req-un [::id]))
(sv/defmethod ::delete-file (sv/defmethod ::delete-file
{::doc/added "1.17" {::doc/added "1.17"
::webhooks/event? true} ::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] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id) (check-edition-permissions! conn profile-id id)
(absorb-library conn params) (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 ;; --- MUTATION COMMAND: link-file-to-library
@ -852,12 +874,13 @@
(db/exec-one! conn [sql:link-file-to-library file-id library-id])) (db/exec-one! conn [sql:link-file-to-library file-id library-id]))
(s/def ::link-file-to-library (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 (sv/defmethod ::link-file-to-library
{::doc/added "1.17" {::doc/added "1.17"
::webhooks/event? true} ::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) (when (= file-id library-id)
(ex/raise :type :validation (ex/raise :type :validation
:code :invalid-library :code :invalid-library
@ -876,12 +899,13 @@
:library-file-id library-id})) :library-file-id library-id}))
(s/def ::unlink-file-from-library (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 (sv/defmethod ::unlink-file-from-library
{::doc/added "1.17" {::doc/added "1.17"
::webhooks/event? true} ::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] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id file-id)
(unlink-file-from-library conn params))) (unlink-file-from-library conn params)))
@ -897,14 +921,15 @@
:library-file-id library-id})) :library-file-id library-id}))
(s/def ::update-file-library-sync-status (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 ;; TODO: improve naming
(sv/defmethod ::update-file-library-sync-status (sv/defmethod ::update-file-library-sync-status
"Update the synchronization statos of a file->library link" "Update the synchronization statos of a file->library link"
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id file-id)
(update-sync conn params))) (update-sync conn params)))
@ -919,13 +944,14 @@
{:id file-id})) {:id file-id}))
(s/def ::ignore-file-library-sync-status (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 ;; TODO: improve naming
(sv/defmethod ::ignore-file-library-sync-status (sv/defmethod ::ignore-file-library-sync-status
"Ignore updates in linked files" "Ignore updates in linked files"
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id file-id)
(ignore-sync conn params))) (ignore-sync conn params)))
@ -948,11 +974,13 @@
(s/def ::data (s/nilable ::us/string)) (s/def ::data (s/nilable ::us/string))
(s/def ::thumbs/object-id ::us/string) (s/def ::thumbs/object-id ::us/string)
(s/def ::upsert-file-object-thumbnail (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 (sv/defmethod ::upsert-file-object-thumbnail
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id file-id)
(upsert-file-object-thumbnail! conn params) (upsert-file-object-thumbnail! conn params)
@ -975,13 +1003,14 @@
(s/def ::revn ::us/integer) (s/def ::revn ::us/integer)
(s/def ::props map?) (s/def ::props map?)
(s/def ::upsert-file-thumbnail (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 (sv/defmethod ::upsert-file-thumbnail
"Creates or updates the file thumbnail. Mainly used for paint the "Creates or updates the file thumbnail. Mainly used for paint the
grid thumbnails." grid thumbnails."
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id file-id)
(upsert-file-thumbnail conn params) (upsert-file-thumbnail conn params)

View file

@ -13,6 +13,7 @@
[app.db :as db] [app.db :as db]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.permissions :as perms] [app.rpc.permissions :as perms]
@ -68,8 +69,8 @@
(files/decode-row file))) (files/decode-row file)))
(s/def ::create-file (s/def ::create-file
(s/keys :req-un [::files/profile-id (s/keys :req [::rpc/profile-id]
::files/name :req-un [::files/name
::files/project-id] ::files/project-id]
:opt-un [::files/id :opt-un [::files/id
::files/is-shared ::files/is-shared
@ -78,10 +79,11 @@
(sv/defmethod ::create-file (sv/defmethod ::create-file
{::doc/added "1.17" {::doc/added "1.17"
::webhooks/event? true} ::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] (db/with-atomic [conn pool]
(proj/check-edition-permissions! conn profile-id project-id) (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) (-> (create-file conn params)
(vary-meta assoc ::audit/props {:team-id team-id}))))) (vary-meta assoc ::audit/props {:team-id team-id})))))

View file

@ -11,6 +11,7 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.files.create :as files.create] [app.rpc.commands.files.create :as files.create]
[app.rpc.commands.files.update :as files.update] [app.rpc.commands.files.update :as files.update]
@ -26,8 +27,8 @@
(s/def ::create-page ::us/boolean) (s/def ::create-page ::us/boolean)
(s/def ::create-temp-file (s/def ::create-temp-file
(s/keys :req-un [::files/profile-id (s/keys :req [::rpc/profile-id]
::files/name :req-un [::files/name
::files/project-id] ::files/project-id]
:opt-un [::files/id :opt-un [::files/id
::files/is-shared ::files/is-shared
@ -36,7 +37,7 @@
(sv/defmethod ::create-temp-file (sv/defmethod ::create-temp-file
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(proj/check-edition-permissions! conn profile-id project-id) (proj/check-edition-permissions! conn profile-id project-id)
(files.create/create-file conn (assoc params :deleted-at (dt/in-future {:days 1}))))) (files.create/create-file conn (assoc params :deleted-at (dt/in-future {:days 1})))))
@ -44,7 +45,7 @@
;; --- MUTATION COMMAND: update-temp-file ;; --- MUTATION COMMAND: update-temp-file
(defn 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 (db/insert! conn :file-change
{:id (uuid/next) {:id (uuid/next)
:session-id session-id :session-id session-id
@ -95,12 +96,12 @@
nil)) nil))
(s/def ::persist-temp-file (s/def ::persist-temp-file
(s/keys :req-un [::files/id (s/keys :req [::rpc/profile-id]
::files/profile-id])) :req-un [::files/id]))
(sv/defmethod ::persist-temp-file (sv/defmethod ::persist-temp-file
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id) (files/check-edition-permissions! conn profile-id id)
(persist-temp-file conn params))) (persist-temp-file conn params)))

View file

@ -20,6 +20,7 @@
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.metrics :as mtx] [app.metrics :as mtx]
[app.msgbus :as mbus] [app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit] [app.rpc.climit :as-alias climit]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@ -52,7 +53,8 @@
(s/def ::revn ::us/integer) (s/def ::revn ::us/integer)
(s/def ::update-file (s/def ::update-file
(s/and (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]) :opt-un [::changes ::changes-with-metadata ::features])
(fn [o] (fn [o]
(or (contains? o :changes) (or (contains? o :changes)
@ -130,19 +132,20 @@
::webhooks/batch-timeout (dt/duration "2m") ::webhooks/batch-timeout (dt/duration "2m")
::webhooks/batch-key :id ::webhooks/batch-key :id
::doc/added "1.17"} ::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] (db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id) (files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id) (db/xact-lock! conn id)
(let [cfg (assoc cfg :conn conn) (let [cfg (assoc cfg :conn conn)
params (assoc params :profile-id profile-id)
tpoint (dt/tpoint)] tpoint (dt/tpoint)]
(-> (update-file cfg params) (-> (update-file cfg params)
(rph/with-defer #(let [elapsed (tpoint)] (rph/with-defer #(let [elapsed (tpoint)]
(l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))) (l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
(defn update-file (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) (let [file (get-file conn id)
features (->> (concat (:features file) features (->> (concat (:features file)
(:features params)) (:features params))
@ -184,7 +187,7 @@
:team-id (:team-id file)})))))) :team-id (:team-id file)}))))))
(defn- update-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) (when (> (:revn params)
(:revn file)) (:revn file))
(ex/raise :type :validation (ex/raise :type :validation

View file

@ -14,6 +14,7 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.binfile :as binfile] [app.rpc.commands.binfile :as binfile]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams :refer [create-project-role create-project]] [app.rpc.commands.teams :as teams :refer [create-project-role create-project]]
@ -31,14 +32,14 @@
(declare duplicate-file) (declare duplicate-file)
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::project-id ::us/uuid) (s/def ::project-id ::us/uuid)
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::duplicate-file (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])) :opt-un [::name]))
(sv/defmethod ::duplicate-file (sv/defmethod ::duplicate-file
@ -47,7 +48,7 @@
::webhooks/event? true} ::webhooks/event? true}
[{:keys [pool] :as cfg} params] [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(duplicate-file conn params))) (duplicate-file conn (assoc params :profile-id (::rpc/profile-id params)))))
(defn- remap-id (defn- remap-id
[item index key] [item index key]
@ -212,7 +213,8 @@
(declare duplicate-project) (declare duplicate-project)
(s/def ::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])) :opt-un [::name]))
(sv/defmethod ::duplicate-project (sv/defmethod ::duplicate-project
@ -221,7 +223,7 @@
::webhooks/event? true} ::webhooks/event? true}
[{:keys [pool] :as cfg} params] [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(duplicate-project conn params))) (duplicate-project conn (assoc params :profile-id (::rpc/profile-id params)))))
(defn duplicate-project (defn duplicate-project
[conn {:keys [profile-id project-id name] :as params}] [conn {:keys [profile-id project-id name] :as params}]
@ -249,9 +251,7 @@
;; create the duplicated project and assign the current profile as ;; create the duplicated project and assign the current profile as
;; a project owner ;; a project owner
(create-project conn project) (create-project conn project)
(create-project-role conn {:project-id (:id project) (create-project-role conn profile-id (:id project) :owner)
:profile-id profile-id
:role :owner})
;; duplicate all files ;; duplicate all files
(let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} 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 ::ids (s/every ::us/uuid :kind set?))
(s/def ::move-files (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 (sv/defmethod ::move-files
"Move a set of files from one project to other." "Move a set of files from one project to other."
@ -330,7 +331,7 @@
::webhooks/event? true} ::webhooks/event? true}
[{:keys [pool] :as cfg} params] [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(move-files conn params))) (move-files conn (assoc params :profile-id (::rpc/profile-id params)))))
;; --- COMMAND: Move project ;; --- COMMAND: Move project
@ -362,7 +363,8 @@
(s/def ::move-project (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 (sv/defmethod ::move-project
"Move projects between teams." "Move projects between teams."
@ -370,7 +372,7 @@
::webhooks/event? true} ::webhooks/event? true}
[{:keys [pool] :as cfg} params] [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(move-project conn params))) (move-project conn (assoc params :profile-id (::rpc/profile-id params)))))
;; --- COMMAND: Clone Template ;; --- COMMAND: Clone Template
@ -378,7 +380,8 @@
(s/def ::template-id ::us/not-empty-string) (s/def ::template-id ::us/not-empty-string)
(s/def ::clone-template (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 (sv/defmethod ::clone-template
"Clone into the specified project the template by its id." "Clone into the specified project the template by its id."
@ -387,7 +390,7 @@
[{:keys [pool] :as cfg} params] [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(-> (assoc cfg :conn conn) (-> (assoc cfg :conn conn)
(clone-template params)))) (clone-template (assoc params :profile-id (::rpc/profile-id params))))))
(defn- clone-template (defn- clone-template
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}] [{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]

View file

@ -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")))))

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.spec :as us] [app.common.spec :as us]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
@ -47,18 +48,18 @@
order by f.created_at asc") order by f.created_at asc")
(defn search-files (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 (db/exec! conn [sql:search-files
profile-id team-id profile-id team-id
profile-id team-id profile-id team-id
search-term])) search-term]))
(s/def ::profile-id ::us/uuid)
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::search-files ::us/string) (s/def ::search-files ::us/string)
(s/def ::search-files (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])) :opt-un [::search-term]))
(sv/defmethod ::search-files (sv/defmethod ::search-files

View file

@ -17,6 +17,7 @@
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.main :as-alias main] [app.main :as-alias main]
[app.media :as media] [app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit] [app.rpc.climit :as climit]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
@ -35,7 +36,6 @@
(s/def ::id ::us/uuid) (s/def ::id ::us/uuid)
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::file-id ::us/uuid) (s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
@ -78,11 +78,11 @@
(declare retrieve-teams) (declare retrieve-teams)
(s/def ::get-teams (s/def ::get-teams
(s/keys :req-un [::profile-id])) (s/keys :req [::rpc/profile-id]))
(sv/defmethod ::get-teams (sv/defmethod ::get-teams
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(retrieve-teams conn profile-id))) (retrieve-teams conn profile-id)))
@ -122,11 +122,12 @@
(declare retrieve-team) (declare retrieve-team)
(s/def ::get-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 (sv/defmethod ::get-team
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(retrieve-team conn profile-id id))) (retrieve-team conn profile-id id)))
@ -161,11 +162,12 @@
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::get-team-members (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 (sv/defmethod ::get-team-members
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(retrieve-team-members conn team-id))) (retrieve-team-members conn team-id)))
@ -177,13 +179,13 @@
(declare retrieve-team-for-file) (declare retrieve-team-for-file)
(s/def ::get-team-users (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]) :opt-un [::team-id ::file-id])
#(or (:team-id %) (:file-id %)))) #(or (:team-id %) (:file-id %))))
(sv/defmethod ::get-team-users (sv/defmethod ::get-team-users
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(if team-id (if team-id
(do (do
@ -236,11 +238,12 @@
(declare retrieve-team-stats) (declare retrieve-team-stats)
(s/def ::get-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 (sv/defmethod ::get-team-stats
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(retrieve-team-stats conn team-id))) (retrieve-team-stats conn team-id)))
@ -257,7 +260,8 @@
;; --- Query: Team invitations ;; --- Query: Team invitations
(s/def ::get-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 (def sql:team-invitations
"select email_to as email, role, (valid_until < now()) as expired "select email_to as email, role, (valid_until < now()) as expired
@ -270,7 +274,7 @@
(sv/defmethod ::get-team-invitations (sv/defmethod ::get-team-invitations
{::doc/added "1.17"} {::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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(get-team-invitations conn team-id))) (get-team-invitations conn team-id)))
@ -285,14 +289,15 @@
(declare ^:private create-team-default-project) (declare ^:private create-team-default-project)
(s/def ::create-team (s/def ::create-team
(s/keys :req-un [::profile-id ::name] (s/keys :req [::rpc/profile-id]
:req-un [::name]
:opt-un [::id])) :opt-un [::id]))
(sv/defmethod ::create-team (sv/defmethod ::create-team
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(create-team conn params))) (create-team conn (assoc params :profile-id profile-id))))
(defn create-team (defn create-team
"This is a complete team creation process, it creates the team "This is a complete team creation process, it creates the team
@ -316,22 +321,20 @@
:is-default is-default}))) :is-default is-default})))
(defn- create-team-role (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 (let [params {:team-id team-id
:profile-id profile-id}] :profile-id profile-id}]
(->> (perms/assign-role-flags params role) (->> (perms/assign-role-flags params role)
(db/insert! conn :team-profile-rel)))) (db/insert! conn :team-profile-rel))))
(defn- create-team-default-project (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) (let [project {:id (uuid/next)
:team-id team-id :team-id team-id
:name "Drafts" :name "Drafts"
:is-default true} :is-default true}
project (create-project conn project)] project (create-project conn project)]
(create-project-role conn {:project-id (:id project) (create-project-role conn profile-id (:id project) :owner)
:profile-id profile-id
:role :owner})
project)) project))
;; NOTE: we have project creation here because there are cyclic ;; NOTE: we have project creation here because there are cyclic
@ -351,7 +354,7 @@
:is-default is-default}))) :is-default is-default})))
(defn create-project-role (defn create-project-role
[conn {:keys [project-id profile-id role]}] [conn profile-id project-id role]
(let [params {:project-id project-id (let [params {:project-id project-id
:profile-id profile-id}] :profile-id profile-id}]
(->> (perms/assign-role-flags params role) (->> (perms/assign-role-flags params role)
@ -360,11 +363,12 @@
;; --- Mutation: Update Team ;; --- Mutation: Update Team
(s/def ::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 (sv/defmethod ::update-team
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id) (check-edition-permissions! conn profile-id id)
(db/update! conn :team (db/update! conn :team
@ -379,11 +383,12 @@
(s/def ::reassign-to ::us/uuid) (s/def ::reassign-to ::us/uuid)
(s/def ::leave-team (s/def ::leave-team
(s/keys :req-un [::profile-id ::id] (s/keys :req [::rpc/profile-id]
:req-un [::id]
:opt-un [::reassign-to])) :opt-un [::reassign-to]))
(defn leave-team (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) (let [perms (get-permissions conn profile-id id)
members (retrieve-team-members conn id)] members (retrieve-team-members conn id)]
@ -438,7 +443,8 @@
;; --- Mutation: Delete Team ;; --- Mutation: Delete Team
(s/def ::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 ;; TODO: right now just don't allow delete default team, in future it
;; should raise a specific exception for signal that this action is ;; should raise a specific exception for signal that this action is
@ -446,7 +452,7 @@
(sv/defmethod ::delete-team (sv/defmethod ::delete-team
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id id)] (let [perms (get-permissions conn profile-id id)]
(when-not (:is-owner perms) (when-not (:is-owner perms)
@ -477,7 +483,7 @@
:viewer {:is-owner false :is-admin false :can-edit false})) :viewer {:is-owner false :is-admin false :can-edit false}))
(defn update-team-member-role (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 ;; We retrieve all team members instead of query the
;; database for a single member. This is just for ;; database for a single member. This is just for
;; convenience, if this becomes a bottleneck or problematic, ;; convenience, if this becomes a bottleneck or problematic,
@ -524,23 +530,25 @@
nil))) nil)))
(s/def ::update-team-member-role (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 (sv/defmethod ::update-team-member-role
{::doc/added "1.17"} {::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} params] [{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool] (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 ;; --- Mutation: Delete Team Member
(s/def ::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 (sv/defmethod ::delete-team-member
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)] (let [perms (get-permissions conn profile-id team-id)]
(when-not (or (:is-owner perms) (when-not (or (:is-owner perms)
@ -564,15 +572,16 @@
(s/def ::file ::media/upload) (s/def ::file ::media/upload)
(s/def ::update-team-photo (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 (sv/defmethod ::update-team-photo
{::doc/added "1.17"} {::doc/added "1.17"}
[cfg {:keys [file] :as params}] [cfg {:keys [::rpc/profile-id file] :as params}]
;; Validate incoming mime type ;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
(let [cfg (update cfg :storage media/configure-assets-storage)] (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 (defn update-team-photo
[{:keys [pool storage executor] :as cfg} {:keys [profile-id team-id] :as params}] [{:keys [pool storage executor] :as cfg} {:keys [profile-id team-id] :as params}]
@ -632,10 +641,10 @@
update set role = ?, updated_at = now();") update set role = ?, updated_at = now();")
(defn- create-invitation-token (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) (tokens/generate (::main/props cfg)
{:iss :team-invitation {:iss :team-invitation
:exp expire :exp valid-until
:profile-id profile-id :profile-id profile-id
:role role :role role
:team-id team-id :team-id team-id
@ -654,7 +663,7 @@
(let [member (profile/retrieve-profile-data-by-email conn email) (let [member (profile/retrieve-profile-data-by-email conn email)
expire (dt/in-future "168h") ;; 7 days expire (dt/in-future "168h") ;; 7 days
itoken (create-invitation-token cfg {:profile-id (:id profile) itoken (create-invitation-token cfg {:profile-id (:id profile)
:expire expire :valid-until expire
:team-id (:id team) :team-id (:id team)
:member-email (or (:email member) email) :member-email (or (:email member) email)
:member-id (:id member) :member-id (:id member)
@ -715,14 +724,15 @@
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::emails ::us/set-of-valid-emails) (s/def ::emails ::us/set-of-valid-emails)
(s/def ::create-team-invitations (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])) :opt-un [::email ::emails]))
(sv/defmethod ::create-team-invitations (sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to "A rpc call that allow to send a single or multiple invitations to
join the team." join the team."
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id) (let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id) profile (db/get-by-id conn :profile profile-id)
@ -760,7 +770,7 @@
(sv/defmethod ::create-team-with-invitations (sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(let [team (create-team conn params) (let [team (create-team conn params)
profile (db/get-by-id conn :profile profile-id) profile (db/get-by-id conn :profile profile-id)
@ -791,11 +801,12 @@
;; --- Query: get-team-invitation-token ;; --- Query: get-team-invitation-token
(s/def ::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 (sv/defmethod ::get-team-invitation-token
{::doc/added "1.17"} {::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) (check-read-permissions! pool profile-id team-id)
(let [invit (-> (db/get pool :team-invitation (let [invit (-> (db/get pool :team-invitation
{:team-id team-id {:team-id team-id
@ -804,7 +815,7 @@
member (profile/retrieve-profile-data-by-email pool (:email invit)) member (profile/retrieve-profile-data-by-email pool (:email invit))
token (create-invitation-token cfg {:team-id (:team-id invit) token (create-invitation-token cfg {:team-id (:team-id invit)
:profile-id profile-id :profile-id profile-id
:expire (:expire invit) :valid-until (:valid-until invit)
:role (:role invit) :role (:role invit)
:member-id (:id member) :member-id (:id member)
:member-email (or (:email member) (:email-to invit))})] :member-email (or (:email member) (:email-to invit))})]
@ -813,11 +824,12 @@
;; --- Mutation: Update invitation role ;; --- Mutation: Update invitation role
(s/def ::update-team-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 (sv/defmethod ::update-team-invitation-role
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)] (let [perms (get-permissions conn profile-id team-id)]
@ -833,11 +845,12 @@
;; --- Mutation: Delete invitation ;; --- Mutation: Delete invitation
(s/def ::delete-team-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 (sv/defmethod ::delete-team-invitation
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)] (let [perms (get-permissions conn profile-id team-id)]

View file

@ -11,6 +11,7 @@
[app.db :as db] [app.db :as db]
[app.http.session :as session] [app.http.session :as session]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
@ -27,10 +28,10 @@
(s/def ::verify-token (s/def ::verify-token
(s/keys :req-un [::token] (s/keys :req-un [::token]
:opt-un [::profile-id])) :opt [::rpc/profile-id]))
(sv/defmethod ::verify-token (sv/defmethod ::verify-token
{:auth false {::rpc/auth false
::doc/added "1.15"} ::doc/added "1.15"}
[{:keys [pool sprops] :as cfg} {:keys [token] :as params}] [{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
@ -126,7 +127,8 @@
:opt-un [::spec.team-invitation/member-id])) :opt-un [::spec.team-invitation/member-id]))
(defmethod process-token :team-invitation (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}] {:keys [member-id team-id member-email] :as claims}]
(us/verify! ::team-invitation-claims claims) (us/verify! ::team-invitation-claims claims)

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.comments :as comments] [app.rpc.commands.comments :as comments]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.cond :as-alias cond] [app.rpc.cond :as-alias cond]
@ -73,16 +74,16 @@
(s/def ::get-view-only-bundle (s/def ::get-view-only-bundle
(s/keys :req-un [::files/file-id] (s/keys :req-un [::files/file-id]
:opt-un [::files/profile-id :opt-un [::files/share-id
::files/share-id ::files/features]
::files/features])) :opt [::rpc/profile-id]))
(sv/defmethod ::get-view-only-bundle (sv/defmethod ::get-view-only-bundle
{:auth false {::rpc/auth false
::cond/get-object #(files/get-minimal-file %1 (:file-id %2)) ::cond/get-object #(files/get-minimal-file %1 (:file-id %2))
::cond/key-fn files/get-file-etag ::cond/key-fn files/get-file-etag
::cond/reuse-key? true ::cond/reuse-key? true
::doc/added "1.17"} ::doc/added "1.17"}
[{:keys [pool]} params] [{:keys [pool]} params]
(with-open [conn (db/open pool)] (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)))))

View file

@ -12,6 +12,7 @@
[app.db :as db] [app.db :as db]
[app.http.client :as http] [app.http.client :as http]
[app.loggers.webhooks :as webhooks] [app.loggers.webhooks :as webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.teams :refer [check-edition-permissions! check-read-permissions!]] [app.rpc.commands.teams :refer [check-edition-permissions! check-read-permissions!]]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]
@ -23,7 +24,6 @@
;; --- Mutation: Create Webhook ;; --- Mutation: Create Webhook
(s/def ::profile-id ::us/uuid)
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::uri ::us/not-empty-string) (s/def ::uri ::us/not-empty-string)
(s/def ::is-active ::us/boolean) (s/def ::is-active ::us/boolean)
@ -33,7 +33,8 @@
"application/transit+json"}) "application/transit+json"})
(s/def ::create-webhook (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])) :opt-un [::is-active]))
;; NOTE: for now the quote is hardcoded but this need to be solved in ;; NOTE: for now the quote is hardcoded but this need to be solved in
@ -98,7 +99,7 @@
(sv/defmethod ::create-webhook (sv/defmethod ::create-webhook
{::doc/added "1.17"} {::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) (check-edition-permissions! pool profile-id team-id)
(validate-quotes! cfg params) (validate-quotes! cfg params)
(->> (validate-webhook! cfg nil params) (->> (validate-webhook! cfg nil params)
@ -109,18 +110,19 @@
(sv/defmethod ::update-webhook (sv/defmethod ::update-webhook
{::doc/added "1.17"} {::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})] (let [whook (db/get pool :webhook {:id id})]
(check-edition-permissions! pool profile-id (:team-id whook)) (check-edition-permissions! pool profile-id (:team-id whook))
(->> (validate-webhook! cfg whook params) (->> (validate-webhook! cfg whook params)
(p/fmap executor (fn [_] (update-webhook! cfg whook params)))))) (p/fmap executor (fn [_] (update-webhook! cfg whook params))))))
(s/def ::delete-webhook (s/def ::delete-webhook
(s/keys :req-un [::profile-id ::id])) (s/keys :req [::rpc/profile-id]
:req-un [::id]))
(sv/defmethod ::delete-webhook (sv/defmethod ::delete-webhook
{::doc/added "1.17"} {::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] (db/with-atomic [conn pool]
(let [whook (db/get conn :webhook {:id id})] (let [whook (db/get conn :webhook {:id id})]
(check-edition-permissions! conn profile-id (:team-id whook)) (check-edition-permissions! conn profile-id (:team-id whook))
@ -131,14 +133,15 @@
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::get-webhooks (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 (def sql:get-webhooks
"select id, uri, mtype, is_active, error_code, error_count "select id, uri, mtype, is_active, error_code, error_count
from webhook where team_id = ? order by uri") from webhook where team_id = ? order by uri")
(sv/defmethod ::get-webhooks (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)] (with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id) (check-read-permissions! conn profile-id team-id)
(db/exec! conn [sql:get-webhooks team-id]))) (db/exec! conn [sql:get-webhooks team-id])))

View file

@ -81,7 +81,8 @@
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(cmd.files/check-edition-permissions! conn profile-id id) (cmd.files/check-edition-permissions! conn profile-id id)
(cmd.files/absorb-library conn params) (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 ;; --- Mutation: Link file to library

View file

@ -6,6 +6,7 @@
(ns app.rpc.mutations.profile (ns app.rpc.mutations.profile
(:require (:require
[app.auth :as auth]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
@ -17,7 +18,7 @@
[app.media :as media] [app.media :as media]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit] [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.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
@ -182,7 +183,7 @@
(defn- change-email-immediately (defn- change-email-immediately
[{:keys [conn]} {:keys [profile email] :as params}] [{:keys [conn]} {:keys [profile email] :as params}]
(when (not= email (:email profile)) (when (not= email (:email profile))
(auth/check-profile-existence! conn params)) (cmd.auth/check-profile-existence! conn params))
(db/update! conn :profile (db/update! conn :profile
{:email email} {:email email}
{:id (:id profile)}) {:id (:id profile)})
@ -201,7 +202,7 @@
:exp (dt/in-future {:days 30})})] :exp (dt/in-future {:days 30})})]
(when (not= email (:email profile)) (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) (when-not (eml/allow-send-emails? conn profile)
(ex/raise :type :validation (ex/raise :type :validation

View file

@ -26,8 +26,6 @@
;; --- Mutation: Create Project ;; --- Mutation: Create Project
(declare create-project-profile-state)
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::create-project (s/def ::create-project
(s/keys :req-un [::profile-id ::team-id ::name] (s/keys :req-un [::profile-id ::team-id ::name]
@ -39,21 +37,16 @@
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}] [{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! conn profile-id team-id)
(let [project (teams/create-project conn params) (let [project (teams/create-project conn params)]
params (assoc params (teams/create-project-role conn profile-id (:id project) :owner)
:project-id (:id project)
:role :owner)]
(teams/create-project-role conn params)
(create-project-profile-state conn params)
(assoc project :is-pinned true))))
(defn create-project-profile-state
[conn {:keys [team-id project-id profile-id] :as params}]
(db/insert! conn :team-project-profile-rel (db/insert! conn :team-project-profile-rel
{:project-id project-id {:project-id (:id project)
:profile-id profile-id :profile-id profile-id
:team-id team-id :team-id team-id
:is-pinned true})) :is-pinned true})
(assoc project :is-pinned true))))
;; --- Mutation: Toggle Project Pin ;; --- Mutation: Toggle Project Pin
@ -122,4 +115,7 @@
{:deleted-at (dt/now)} {:deleted-at (dt/now)}
{:id id :is-default false})] {:id id :is-default false})]
(rph/with-meta (rph/wrap) (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)}}))))

View file

@ -10,6 +10,7 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc]
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -36,7 +37,7 @@
(s/keys :opt-un [::profile-id])) (s/keys :opt-un [::profile-id]))
(sv/defmethod ::profile (sv/defmethod ::profile
{:auth false} {::rpc/auth false}
[{:keys [pool] :as cfg} {:keys [profile-id] :as params}] [{:keys [pool] :as cfg} {:keys [profile-id] :as params}]
;; We need to return the anonymous profile object in two cases, when ;; 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 ;; no profile-id is in session, and when db call raises not found. In all other

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.spec :as us] [app.common.spec :as us]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.viewer :as viewer] [app.rpc.commands.viewer :as viewer]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.util.services :as sv] [app.util.services :as sv]
@ -19,7 +20,7 @@
(s/keys :opt-un [::components-v2]))) (s/keys :opt-un [::components-v2])))
(sv/defmethod ::view-only-bundle (sv/defmethod ::view-only-bundle
{:auth false {::rpc/auth false
::doc/added "1.3" ::doc/added "1.3"
::doc/deprecated "1.17"} ::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [features components-v2] :as params}] [{:keys [pool] :as cfg} {:keys [features components-v2] :as params}]

View file

@ -13,6 +13,7 @@
[app.db :as db] [app.db :as db]
[app.main :as-alias main] [app.main :as-alias main]
[app.setup.builtin-templates] [app.setup.builtin-templates]
[app.setup.initial-user]
[app.setup.keys :as keys] [app.setup.keys :as keys]
[buddy.core.codecs :as bc] [buddy.core.codecs :as bc]
[buddy.core.nonce :as bn] [buddy.core.nonce :as bn]

View file

@ -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))

View file

@ -8,6 +8,7 @@
"A main namespace for server repl." "A main namespace for server repl."
#_:clj-kondo/ignore #_:clj-kondo/ignore
(:require (:require
[app.auth :refer [derive-password]]
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
@ -20,7 +21,6 @@
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.main :refer [system]] [app.main :refer [system]]
[app.rpc.commands.auth :refer [derive-password]]
[app.rpc.queries.profile :as prof] [app.rpc.queries.profile :as prof]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.time :as dt] [app.util.time :as dt]

View file

@ -6,7 +6,7 @@
(ns app.tasks.objects-gc (ns app.tasks.objects-gc
"A maintenance task that performs a general purpose garbage collection "A maintenance task that performs a general purpose garbage collection
of deleted objects." of deleted or unreachable objects."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.logging :as l] [app.common.logging :as l]
@ -16,154 +16,247 @@
[app.storage :as sto] [app.storage :as sto]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig])) [integrant.core :as ig]))
(def target-tables (declare ^:private delete-profiles!)
["profile" (declare ^:private delete-teams!)
"team" (declare ^:private delete-fonts!)
"file" (declare ^:private delete-projects!)
"project" (declare ^:private delete-files!)
"team_font_variant"]) (declare ^:private delete-orphan-teams!)
(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)))))
(s/def ::min-age ::dt/duration) (s/def ::min-age ::dt/duration)
(defmethod ig/pre-init-spec ::handler [_] (defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::sto/storage] (s/keys :req [::db/pool ::sto/storage]
:opt-un [::min-age])) :opt [::min-age]))
(defmethod ig/prep-key ::handler (defmethod ig/prep-key ::handler
[_ cfg] [_ cfg]
(merge {:min-age cf/deletion-delay} (merge {::min-age cf/deletion-delay}
(d/without-nils cfg))) (d/without-nils cfg)))
(defmethod ig/init-key ::handler (defmethod ig/init-key ::handler
[_ {:keys [pool] :as cfg}] [_ {:keys [::db/pool ::sto/storage] :as cfg}]
(fn [params] (fn [params]
(db/with-atomic [conn pool] (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))
cfg (-> cfg _ (l/info :hint "gc started"
(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) :min-age (dt/format-duration min-age)
:total total) :rollback? (boolean (:rollback? params)))
storage (media/configure-assets-storage storage conn)
cfg (-> cfg
(assoc ::min-age (db/interval min-age))
(assoc ::conn conn)
(assoc ::storage storage))
htotal (+ (delete-profiles! cfg)
(delete-teams! cfg)
(delete-projects! cfg)
(delete-files! cfg)
(delete-fonts! cfg))
stotal (delete-orphan-teams! cfg)]
(l/info :hint "gc finished"
:deleted htotal
:orphans stotal
:rollback? (boolean (:rollback? params)))
(when (:rollback? params) (when (:rollback? params)
(db/rollback! conn)) (db/rollback! conn))
{:processed total}))))))) {: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)))))

View file

@ -6,6 +6,7 @@
(ns backend-tests.helpers (ns backend-tests.helpers
(:require (:require
[app.auth]
[app.common.data :as d] [app.common.data :as d]
[app.common.flags :as flags] [app.common.flags :as flags]
[app.common.pages :as cp] [app.common.pages :as cp]
@ -17,6 +18,7 @@
[app.main :as main] [app.main :as main]
[app.media] [app.media]
[app.migrations] [app.migrations]
[app.rpc :as-alias rpc]
[app.rpc.commands.auth :as cmd.auth] [app.rpc.commands.auth :as cmd.auth]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.files.create :as files.create] [app.rpc.commands.files.create :as files.create]
@ -101,8 +103,9 @@
*pool* (:app.db/pool system)] *pool* (:app.db/pool system)]
(with-redefs [app.config/flags (flags/parse flags/default default-flags (:flags config)) (with-redefs [app.config/flags (flags/parse flags/default default-flags (:flags config))
app.config/config config app.config/config config
app.rpc.commands.auth/derive-password identity app.loggers.audit/submit! (constantly nil)
app.rpc.commands.auth/verify-password (fn [a b] {:valid (= a b)})] app.auth/derive-password identity
app.auth/verify-password (fn [a b] {:valid (= a b)})]
(next))) (next)))
(finally (finally
(ig/halt! system))))) (ig/halt! system)))))
@ -322,14 +325,21 @@
(try-on! (method-fn (dissoc data ::type))))) (try-on! (method-fn (dissoc data ::type)))))
(defn mutation! (defn mutation!
[{:keys [::type] :as data}] [{:keys [::type profile-id] :as data}]
(let [method-fn (get-in *system* [:app.rpc/methods :mutations type])] (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! (defn query!
[{:keys [::type] :as data}] [{:keys [::type profile-id] :as data}]
(let [method-fn (get-in *system* [:app.rpc/methods :queries type])] (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! (defn run-task!
([name] ([name]

View file

@ -65,8 +65,7 @@
;; Refresh webhook ;; Refresh webhook
(let [whk' (th/db-get :webhook {:id (:id whk)})] (let [whk' (th/db-get :webhook {:id (:id whk)})]
(t/is (nil? (:error-code whk'))) (t/is (nil? (:error-code whk'))))
(prn whk'))
))) )))

View file

@ -10,6 +10,7 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.util.time :as dt] [app.util.time :as dt]
[app.rpc :as-alias rpc]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t])) [clojure.test :as t]))
@ -37,7 +38,7 @@
params {::th/type :push-audit-events params {::th/type :push-audit-events
:app.http/request http-request :app.http/request http-request
:profile-id (:id prof) ::rpc/profile-id (:id prof)
:events [{:name "navigate" :events [{:name "navigate"
:props {:project-id proj-id :props {:project-id proj-id
:team-id team-id :team-id team-id
@ -67,7 +68,7 @@
params {::th/type :push-audit-events params {::th/type :push-audit-events
:app.http/request http-request :app.http/request http-request
:profile-id (:id prof) ::rpc/profile-id (:id prof)
:events [{:name "navigate" :events [{:name "navigate"
:props {:project-id proj-id :props {:project-id proj-id
:team-id team-id :team-id team-id

View file

@ -6,12 +6,13 @@
(ns backend-tests.rpc-cond-middleware-test (ns backend-tests.rpc-cond-middleware-test
(:require (:require
[backend-tests.storage-test :refer [configure-storage-backend]]
[backend-tests.helpers :as th]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc]
[app.rpc.cond :as cond] [app.rpc.cond :as cond]
[backend-tests.helpers :as th]
[backend-tests.storage-test :refer [configure-storage-backend]]
[clojure.test :as t] [clojure.test :as t]
[datoteka.core :as fs])) [datoteka.core :as fs]))
@ -24,7 +25,9 @@
:profile-id (:id profile)}) :profile-id (:id profile)})
file1 (th/create-file* 1 {:profile-id (:id profile) file1 (th/create-file* 1 {:profile-id (:id profile)
:project-id (:id project)}) :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] (binding [cond/*enabled* true]
(let [{:keys [error result]} (th/command! params)] (let [{:keys [error result]} (th/command! params)]

View file

@ -583,6 +583,7 @@
:object-id (str page-id frame1-id) :object-id (str page-id frame1-id)
:data nil} :data nil}
{:keys [error result] :as out} (th/mutation! data)] {:keys [error result] :as out} (th/mutation! data)]
;; (th/print-result! out)
(t/is (nil? error)) (t/is (nil? error))
(t/is (nil? result))) (t/is (nil? result)))

View file

@ -6,12 +6,13 @@
(ns backend-tests.rpc-management-test (ns backend-tests.rpc-management-test
(:require (:require
[backend-tests.storage-test :refer [configure-storage-backend]]
[backend-tests.helpers :as th]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc]
[app.storage :as sto] [app.storage :as sto]
[backend-tests.helpers :as th]
[backend-tests.storage-test :refer [configure-storage-backend]]
[buddy.core.bytes :as b] [buddy.core.bytes :as b]
[clojure.test :as t] [clojure.test :as t]
[datoteka.core :as fs])) [datoteka.core :as fs]))
@ -50,7 +51,7 @@
:object (select-keys mobj [:id :width :height :mtype :name])}]}) :object (select-keys mobj [:id :width :height :mtype :name])}]})
(let [data {::th/type :duplicate-file (let [data {::th/type :duplicate-file
:profile-id (:id profile) ::rpc/profile-id (:id profile)
:file-id (:id file1) :file-id (:id file1)
:name "file 1 (copy)"} :name "file 1 (copy)"}
out (th/command! data)] out (th/command! data)]
@ -122,7 +123,7 @@
@(sto/del-object! storage sobject) @(sto/del-object! storage sobject)
(let [data {::th/type :duplicate-file (let [data {::th/type :duplicate-file
:profile-id (:id profile) ::rpc/profile-id (:id profile)
:file-id (:id file1) :file-id (:id file1)
:name "file 1 (copy)"} :name "file 1 (copy)"}
out (th/command! data)] out (th/command! data)]
@ -184,7 +185,7 @@
(let [data {::th/type :duplicate-project (let [data {::th/type :duplicate-project
:profile-id (:id profile) ::rpc/profile-id (:id profile)
:project-id (:id project) :project-id (:id project)
:name "project 1 (copy)"} :name "project 1 (copy)"}
out (th/command! data)] out (th/command! data)]
@ -250,7 +251,7 @@
(th/mark-file-deleted* {:id (:id file1)}) (th/mark-file-deleted* {:id (:id file1)})
(let [data {::th/type :duplicate-project (let [data {::th/type :duplicate-project
:profile-id (:id profile) ::rpc/profile-id (:id profile)
:project-id (:id project) :project-id (:id project)
:name "project 1 (copy)"} :name "project 1 (copy)"}
out (th/command! data)] out (th/command! data)]
@ -313,7 +314,7 @@
;; Try to move to same project ;; Try to move to same project
(let [data {::th/type :move-files (let [data {::th/type :move-files
:profile-id (:id profile) ::rpc/profile-id (:id profile)
:project-id (:id project1) :project-id (:id project1)
:ids #{(:id file1)}} :ids #{(:id file1)}}
@ -333,7 +334,7 @@
;; move a file1 to project2 (in the same team) ;; move a file1 to project2 (in the same team)
(let [data {::th/type :move-files (let [data {::th/type :move-files
:profile-id (:id profile) ::rpc/profile-id (:id profile)
:project-id (:id project2) :project-id (:id project2)
:ids #{(:id file1)}} :ids #{(:id file1)}}
@ -416,7 +417,7 @@
;; move to other project in other team ;; move to other project in other team
(let [data {::th/type :move-files (let [data {::th/type :move-files
:profile-id (:id profile) ::rpc/profile-id (:id profile)
:project-id (:id project2) :project-id (:id project2)
:ids #{(:id file1)}} :ids #{(:id file1)}}
out (th/command! data)] out (th/command! data)]
@ -489,7 +490,7 @@
;; move the library to other project ;; move the library to other project
(let [data {::th/type :move-files (let [data {::th/type :move-files
:profile-id (:id profile) ::rpc/profile-id (:id profile)
:project-id (:id project2) :project-id (:id project2)
:ids #{(:id file2)}} :ids #{(:id file2)}}
out (th/command! data)] out (th/command! data)]
@ -575,7 +576,7 @@
;; move project1 to other team ;; move project1 to other team
;; TODO: correct team change of project ;; TODO: correct team change of project
(let [data {::th/type :move-project (let [data {::th/type :move-project
:profile-id (:id profile) ::rpc/profile-id (:id profile)
:project-id (:id project1) :project-id (:id project1)
:team-id (:id team)} :team-id (:id team)}
out (th/command! data)] out (th/command! data)]
@ -608,7 +609,7 @@
(t/deftest clone-template (t/deftest clone-template
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
data {::th/type :clone-template data {::th/type :clone-template
:profile-id (:id prof) ::rpc/profile-id (:id prof)
:project-id (:default-project-id prof) :project-id (:default-project-id prof)
:template-id "test"} :template-id "test"}
@ -624,7 +625,7 @@
(t/deftest retrieve-list-of-buitin-templates (t/deftest retrieve-list-of-buitin-templates
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
data {::th/type :retrieve-list-of-builtin-templates data {::th/type :retrieve-list-of-builtin-templates
:profile-id (:id prof)} ::rpc/profile-id (:id prof)}
out (th/command! data)] out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))

View file

@ -146,7 +146,12 @@
;; execute permanent deletion task ;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age (dt/duration "-1m")})] (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 ;; query profile after delete
(let [params {::th/type :profile (let [params {::th/type :profile

View file

@ -6,13 +6,14 @@
(ns backend-tests.rpc-team-test (ns backend-tests.rpc-team-test
(:require (:require
[backend-tests.helpers :as th]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc]
[app.storage :as sto] [app.storage :as sto]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.time :as dt] [app.util.time :as dt]
[backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
[datoteka.core :as fs] [datoteka.core :as fs]
[mockery.core :refer [with-mocks]])) [mockery.core :refer [with-mocks]]))
@ -65,7 +66,7 @@
;; get invitation token ;; get invitation token
(let [params {::th/type :get-team-invitation-token (let [params {::th/type :get-team-invitation-token
:profile-id (:id profile1) ::rpc/profile-id (:id profile1)
:team-id (:id team) :team-id (:id team)
:email "foo@bar.com"} :email "foo@bar.com"}
out (th/command! params)] out (th/command! params)]
@ -214,7 +215,7 @@
:role "editor" :role "editor"
:valid-until (dt/in-future "48h")}) :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)] out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (th/success? out)) (t/is (th/success? out))
@ -235,7 +236,7 @@
:role "editor" :role "editor"
:valid-until (dt/in-future "48h")}) :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)] out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (not (th/success? out))) (t/is (not (th/success? out)))

View file

@ -100,6 +100,7 @@
out (th/query! data)] out (th/query! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (contains? result :file)) (t/is (contains? result :file))
(t/is (contains? result :project))))) (t/is (contains? result :project)))))

View file

@ -10,6 +10,7 @@
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.storage :as sto] [app.storage :as sto]
[app.rpc :as-alias rpc]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
[mockery.core :refer [with-mocks]])) [mockery.core :refer [with-mocks]]))
@ -28,7 +29,7 @@
(t/testing "create webhook" (t/testing "create webhook"
(let [params {::th/type :create-webhook (let [params {::th/type :create-webhook
:profile-id (:id prof) ::rpc/profile-id (:id prof)
:team-id team-id :team-id team-id
:uri "http://example.com" :uri "http://example.com"
:mtype "application/json"} :mtype "application/json"}
@ -54,7 +55,7 @@
(t/testing "update webhook 1 (success)" (t/testing "update webhook 1 (success)"
(let [params {::th/type :update-webhook (let [params {::th/type :update-webhook
:profile-id (:id prof) ::rpc/profile-id (:id prof)
:id (:id @whook) :id (:id @whook)
:uri (:uri @whook) :uri (:uri @whook)
:mtype "application/transit+json" :mtype "application/transit+json"
@ -82,7 +83,7 @@
(t/testing "update webhook 2 (change uri)" (t/testing "update webhook 2 (change uri)"
(let [params {::th/type :update-webhook (let [params {::th/type :update-webhook
:profile-id (:id prof) ::rpc/profile-id (:id prof)
:id (:id @whook) :id (:id @whook)
:uri (str (:uri @whook) "/test") :uri (str (:uri @whook) "/test")
:mtype "application/transit+json" :mtype "application/transit+json"
@ -97,7 +98,7 @@
(t/testing "update webhook 3 (not authorized)" (t/testing "update webhook 3 (not authorized)"
(let [params {::th/type :update-webhook (let [params {::th/type :update-webhook
:profile-id uuid/zero ::rpc/profile-id uuid/zero
:id (:id @whook) :id (:id @whook)
:uri (str (:uri @whook) "/test") :uri (str (:uri @whook) "/test")
:mtype "application/transit+json" :mtype "application/transit+json"
@ -115,7 +116,7 @@
(t/testing "delete webhook (success)" (t/testing "delete webhook (success)"
(let [params {::th/type :delete-webhook (let [params {::th/type :delete-webhook
:profile-id (:id prof) ::rpc/profile-id (:id prof)
:id (:id @whook)} :id (:id @whook)}
out (th/command! params)] out (th/command! params)]
@ -128,7 +129,7 @@
(t/testing "delete webhook (unauthorozed)" (t/testing "delete webhook (unauthorozed)"
(let [params {::th/type :delete-webhook (let [params {::th/type :delete-webhook
:profile-id uuid/zero ::rpc/profile-id uuid/zero
:id (:id @whook)} :id (:id @whook)}
out (th/command! params)] out (th/command! params)]
@ -149,7 +150,7 @@
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof) team-id (:default-team-id prof)
params {::th/type :create-webhook params {::th/type :create-webhook
:profile-id (:id prof) ::rpc/profile-id (:id prof)
:team-id team-id :team-id team-id
:uri "http://example.com" :uri "http://example.com"
:mtype "application/json"} :mtype "application/json"}

View file

@ -12,8 +12,7 @@
(def default (def default
"A common flags that affects both: backend and frontend." "A common flags that affects both: backend and frontend."
[:enable-registration [:enable-registration
:enable-login :enable-login-with-password])
:enable-webhooks])
(defn parse (defn parse
[& flags] [& flags]

View file

@ -14,6 +14,7 @@
[lambdaisland.uri :as luri] [lambdaisland.uri :as luri]
[linked.core :as lk] [linked.core :as lk]
[linked.set :as lks] [linked.set :as lks]
#?(:clj [datoteka.fs :as fs])
#?(:cljs ["luxon" :as lxn])) #?(:cljs ["luxon" :as lxn]))
#?(:clj #?(:clj
(:import (:import
@ -22,6 +23,7 @@
java.io.ByteArrayInputStream java.io.ByteArrayInputStream
java.io.ByteArrayOutputStream java.io.ByteArrayOutputStream
java.io.File java.io.File
java.nio.file.Path
java.time.Duration java.time.Duration
java.time.Instant java.time.Instant
java.time.OffsetDateTime java.time.OffsetDateTime
@ -102,11 +104,15 @@
;; --- HANDLERS ;; --- HANDLERS
(add-handlers! (add-handlers!
#?(:clj #?@(:clj
{:id "file" [{:id "file"
:class File :class File
:wfn str :wfn str
:rfn identity}) :rfn identity}
{:id "path"
:class Path
:wfn str
:rfn fs/path}])
#?(:cljs #?(:cljs
{:id "n" {:id "n"

View file

@ -113,6 +113,10 @@ http {
proxy_pass http://127.0.0.1:6060/api; proxy_pass http://127.0.0.1:6060/api;
} }
location /admin {
proxy_pass http://127.0.0.1:6063/admin;
}
location /webhooks { location /webhooks {
proxy_pass http://127.0.0.1:6060/webhooks; proxy_pass http://127.0.0.1:6060/webhooks;
} }

View file

@ -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 FROM ubuntu:22.04
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>" LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
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' COPY --from=jre-build /opt/jre /opt/jre
WORKDIR /root
RUN set -ex; \ RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
apt-get -qq update; \ apt-get -qq update; \
apt-get -qqy --no-install-recommends install \ apt-get -qqy --no-install-recommends install \
curl \ curl \
@ -23,34 +80,8 @@ RUN set -ex; \
locale-gen; \ locale-gen; \
rm -rf /var/lib/apt/lists/*; rm -rf /var/lib/apt/lists/*;
RUN set -eux; \ COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
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;
ENV JAVA_HOME=/usr/lib/jvm/openjdk PATH="/usr/lib/jvm/openjdk/bin:$PATH" USER penpot:penpot
ADD ./bundle-backend/ /opt/penpot/backend/
WORKDIR /opt/penpot/backend WORKDIR /opt/penpot/backend
CMD ["/bin/bash", "run.sh"] CMD ["/bin/bash", "run.sh"]

View file

@ -1,20 +1,28 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>" LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive
ENV LANG=en_US.UTF-8 \ ENV LANG=en_US.UTF-8 \
LC_ALL=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; \ RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
mkdir -p /etc/resolvconf/resolv.conf.d; \ 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 -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; \ echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
locale-gen; \ locale-gen;
rm -rf /var/lib/apt/lists/*;
RUN set -ex; \ RUN set -ex; \
apt-get -qq update; \ apt-get -qq update; \
@ -63,8 +71,6 @@ RUN set -ex; \
; \ ; \
rm -rf /var/lib/apt/lists/*; rm -rf /var/lib/apt/lists/*;
ENV PATH="/usr/local/nodejs/bin:$PATH"
RUN set -eux; \ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \ ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \ case "${ARCH}" in \
@ -83,19 +89,22 @@ RUN set -eux; \
;; \ ;; \
esac; \ esac; \
curl -LfsSo /tmp/nodejs.tar.xz ${BINARY_URL}; \ curl -LfsSo /tmp/nodejs.tar.xz ${BINARY_URL}; \
mkdir -p /usr/local/nodejs; \ mkdir -p /opt/node; \
cd /usr/local/nodejs; \ cd /opt/node; \
tar -xf /tmp/nodejs.tar.xz --strip-components=1; \ tar -xf /tmp/nodejs.tar.xz --strip-components=1; \
chown -R root /usr/local/nodejs; \ chown -R root /opt/node; \
/usr/local/nodejs/bin/npm install -g yarn; \ npm install -g yarn; \
rm -rf /tmp/nodejs.tar.xz; 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; \ RUN set -ex; \
yarn; \ yarn; \
npx playwright install chromium; yarn run playwright install chromium;
CMD ["/usr/local/nodejs/bin/node", "app.js"] CMD ["node", "app.js"]

View file

@ -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

View file

@ -5,20 +5,43 @@ networks:
penpot: penpot:
volumes: volumes:
penpot_postgres_data: penpot_postgres_v15:
penpot_assets_data: penpot_assets:
# penpot_traefik:
# penpot_minio:
services: 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=<EMAIL_ADDRESS>"
# - "--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: penpot-frontend:
image: "penpotapp/frontend:latest" image: "penpotapp/frontend:latest"
ports: ports:
- 9001:80 - 9001:80
volumes: volumes:
- penpot_assets_data:/opt/data - penpot_assets:/opt/data
env_file:
- config.env
depends_on: depends_on:
- penpot-backend - penpot-backend
@ -27,51 +50,250 @@ services:
networks: networks:
- penpot - 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(`<DOMAIN_NAME>`)"
# - "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(`<DOMAIN_NAME>`)"
# - "traefik.http.routers.penpot-http.middlewares=http-redirect"
# - "traefik.http.routers.penpot-https.entrypoints=websecure"
# - "traefik.http.routers.penpot-https.rule=Host(`<DOMAIN_NAME>`)"
# - "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: penpot-backend:
image: "penpotapp/backend:latest" image: "penpotapp/backend:latest"
volumes: volumes:
- penpot_assets_data:/opt/data - penpot_assets:/opt/data
depends_on: depends_on:
- penpot-postgres - penpot-postgres
- penpot-redis - penpot-redis
env_file:
- config.env
networks: networks:
- penpot - 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=<KEY_ID>
# - AWS_SECRET_ACCESS_KEY=<ACCESS_KEY>
# - PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
# - PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://penpot-minio:9000
# - PENPOT_STORAGE_ASSETS_S3_BUCKET=<BUKET_NAME>
## 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: penpot-exporter:
image: "penpotapp/exporter:latest" image: "penpotapp/exporter:latest"
env_file: networks:
- config.env - penpot
environment: environment:
# Don't touch it; this uses internal docker network to # Don't touch it; this uses internal docker network to
# communicate with the frontend. # communicate with the frontend.
- PENPOT_PUBLIC_URI=http://penpot-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: penpot-postgres:
image: "postgres:14" image: "postgres:15"
restart: always restart: always
stop_signal: SIGINT stop_signal: SIGINT
volumes:
- penpot_postgres_v15:/var/lib/postgresql/data
networks:
- penpot
environment: environment:
- POSTGRES_INITDB_ARGS=--data-checksums - POSTGRES_INITDB_ARGS=--data-checksums
- POSTGRES_DB=penpot - POSTGRES_DB=penpot
- POSTGRES_USER=penpot - POSTGRES_USER=penpot
- POSTGRES_PASSWORD=penpot - POSTGRES_PASSWORD=penpot
volumes:
- penpot_postgres_data:/var/lib/postgresql/data
networks:
- penpot
penpot-redis: penpot-redis:
image: redis:7 image: redis:7
restart: always restart: always
networks: networks:
- penpot - 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

View file

@ -25,7 +25,7 @@ http {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
error_log /dev/stdout; error_log /dev/stderr;
access_log /dev/stdout; access_log /dev/stdout;
gzip on; gzip on;
@ -60,29 +60,6 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
etag off; 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 { location @handle_redirect {
set $redirect_uri "$upstream_http_location"; set $redirect_uri "$upstream_http_location";
@ -116,5 +93,35 @@ http {
alias /opt/data/assets; alias /opt/data/assets;
add_header x-internal-redirect "$upstream_http_x_accel_redirect"; 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/;
}
} }
} }

View file

@ -44,3 +44,6 @@
(proc/on "uncaughtException" (proc/on "uncaughtException"
(fn [cause] (fn [cause]
(js/console.error cause))) (js/console.error cause)))
(proc/on "SIGTERM" (fn [] (proc/exit 0)))
(proc/on "SIGINT" (fn [] (proc/exit 0)))

View file

@ -102,6 +102,10 @@
(= :profile-blocked (:code cause))) (= :profile-blocked (:code cause)))
(reset! error (tr "errors.profile-blocked")) (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)) (and (= :validation (:type cause))
(= :wrong-credentials (:code cause))) (= :wrong-credentials (:code cause)))
(reset! error (tr "errors.wrong-credentials")) (reset! error (tr "errors.wrong-credentials"))
@ -167,7 +171,8 @@
:label (tr "auth.password")}]] :label (tr "auth.password")}]]
[:div.buttons-stack [:div.buttons-stack
(when (contains? @cf/flags :login) (when (or (contains? @cf/flags :login)
(contains? @cf/flags :login-with-password))
[:& fm/submit-button [:& fm/submit-button
{:label (tr "auth.login-submit") {:label (tr "auth.login-submit")
:data-test "login-submit"}]) :data-test "login-submit"}])
@ -228,6 +233,7 @@
[:& login-buttons {:params params}] [:& login-buttons {:params params}]
(when (or (contains? @cf/flags :login) (when (or (contains? @cf/flags :login)
(contains? @cf/flags :login-with-password)
(contains? @cf/flags :login-with-ldap)) (contains? @cf/flags :login-with-ldap))
[:span.separator [:span.separator
[:span.line] [:span.line]
@ -235,6 +241,7 @@
[:span.line]])]) [:span.line]])])
(when (or (contains? @cf/flags :login) (when (or (contains? @cf/flags :login)
(contains? @cf/flags :login-with-password)
(contains? @cf/flags :login-with-ldap)) (contains? @cf/flags :login-with-ldap))
[:& login-form {:params params :on-success-callback on-success-callback}])]) [:& login-form {:params params :on-success-callback on-success-callback}])])
@ -247,7 +254,8 @@
[:& login-methods {:params params}] [:& login-methods {:params params}]
[:div.links [:div.links
(when (contains? @cf/flags :login) (when (or (contains? @cf/flags :login)
(contains? @cf/flags :login-with-password))
[:div.link-entry [:div.link-entry
[:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request)) [:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request))
:data-test "forgot-password"} :data-test "forgot-password"}

View file

@ -1441,7 +1441,7 @@ msgstr "Resend invitation"
#: src/app/main/ui/dashboard/team.cljs #: src/app/main/ui/dashboard/team.cljs
msgid "labels.copy-invitation-link" 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 #: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
msgid "labels.retry" msgid "labels.retry"

View file

@ -1611,7 +1611,7 @@ msgstr "Reenviar invitacion"
#: src/app/main/ui/dashboard/team.cljs #: src/app/main/ui/dashboard/team.cljs
msgid "labels.copy-invitation-link" 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 #: src/app/main/ui/static.cljs, src/app/main/ui/static.cljs
msgid "labels.retry" msgid "labels.retry"

View file

@ -1,5 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex
export ORGANIZATION="penpotapp"; export ORGANIZATION="penpotapp";
export DEVENV_IMGNAME="$ORGANIZATION/devenv"; 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_HASH=$(git rev-parse --short HEAD);
export CURRENT_COMMITS=$(git rev-list --count HEAD) export CURRENT_COMMITS=$(git rev-list --count HEAD)
set -ex
function print-current-version { function print-current-version {
echo -n "$CURRENT_VERSION-$CURRENT_COMMITS-g$CURRENT_HASH" 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 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 if [[ ! $(docker ps -f "name=penpot-devenv-main" -q) ]]; then
start-devenv start-devenv
fi fi
@ -86,6 +87,14 @@ function run-devenv {
docker exec -ti penpot-devenv-main sudo -EH -u penpot /home/start-tmux.sh 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 { function build {
echo ">> build start: $1" echo ">> build start: $1"
local version=$(print-current-version); local version=$(print-current-version);
@ -164,6 +173,20 @@ function build-exporter-bundle {
echo ">> bundle exporter end"; 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 { function usage {
echo "PENPOT build & release manager" echo "PENPOT build & release manager"
echo "USAGE: $0 OPTION" echo "USAGE: $0 OPTION"
@ -203,7 +226,10 @@ case $1 in
start-devenv ${@:2} start-devenv ${@:2}
;; ;;
run-devenv) run-devenv)
run-devenv ${@:2} run-devenv-tmux ${@:2}
;;
run-devenv-shell)
run-devenv-shell ${@:2}
;; ;;
stop-devenv) stop-devenv)
stop-devenv ${@:2} stop-devenv ${@:2}
@ -216,6 +242,12 @@ case $1 in
;; ;;
# production builds # production builds
build-bundle)
build-frontend-bundle;
build-backend-bundle;
build-exporter-bundle;
;;
build-frontend-bundle) build-frontend-bundle)
build-frontend-bundle; build-frontend-bundle;
;; ;;
@ -228,6 +260,10 @@ case $1 in
build-exporter-bundle; build-exporter-bundle;
;; ;;
build-docker-images)
build-docker-images
;;
# Docker Image Tasks # Docker Image Tasks
*) *)
usage usage