Compare commits

..

11 Commits

Author SHA1 Message Date
Andrés Moya
069995b62f more wip 2022-12-14 16:56:01 +01:00
Andrés Moya
d145716e52 this not fine 2022-12-14 16:56:01 +01:00
Andrés Moya
a4916b8add wip otro más 2022-12-14 16:56:01 +01:00
Andrés Moya
14ee85ccd7 temporary deactivation 2022-12-14 16:56:01 +01:00
Andrés Moya
e8bf9d5f41 wip mejor 2022-12-14 16:56:01 +01:00
Andrés Moya
5f33f54cff 💄 Do some cleanup 2022-12-14 16:56:01 +01:00
Andrés Moya
7fbc47ccdb Avoid setting touched flags in copies 2022-12-14 16:56:01 +01:00
Andrés Moya
8ccd9bedfa wip del gueno 2022-12-14 16:56:01 +01:00
Andrés Moya
a4e36390e2 wip fixes 2022-12-14 16:56:01 +01:00
Andrés Moya
4eaa9394f6 🎉 Show transient changes in component copies 2022-12-14 16:56:01 +01:00
Andrés Moya
9bf0c6e681 🐛 Avoid loops in main instance sync 2022-12-14 16:56:01 +01:00
156 changed files with 3211 additions and 3799 deletions

View File

@@ -45,15 +45,6 @@
:redundant-do
{:level :off}
:earmuffed-var-not-dynamic
{:level :off}
:dynamic-var-not-earmuffed
{:level :off}
:used-underscored-binding
{:level :warning}
:unused-binding
{:exclude-destructured-as true
:exclude-destructured-keys-in-fn-args false

View File

@@ -3,7 +3,6 @@
## :rocket: Next (1.17)
### :boom: Breaking changes & Deprecations
### :sparkles: New features
- Adds layout flex functionality for boards
@@ -20,15 +19,10 @@
- Fix twitter support account link [Taiga #4279](https://tree.taiga.io/project/penpot/issue/4279)
- Fix lang autodetect issue [Taiga #4277](https://tree.taiga.io/project/penpot/issue/4277)
- Fix adding an extra page on import [Taiga #4543](https://tree.taiga.io/project/penpot/task/4543)
- Fix unable to select text at assets inputs in firefox [Taiga #4572](https://tree.taiga.io/project/penpot/issue/4572)
- Fix component sync when converting to path [Taiga #3642](https://tree.taiga.io/project/penpot/issue/3642)
### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!)
## 1.16.2-beta
- Fix strage cursor behaviour after clicking viewport with text pool [Github #2447](https://github.com/penpot/penpot/issues/2447)
## 1.16.1-beta

View File

@@ -6,21 +6,14 @@
<div class="tags">
{% if item.deprecated %}
<span class="tag">
<span>DEPRECATED</span>
</span>
{% endif %}
{% if item.auth %}
<span class="tag">
<span>AUTH</span>
</span>
{% endif %}
{% if item.webhook %}
<span class="tag">
<span>WEBHOOK</span>
<span>Deprecated:</span>
<span>since v{{item.deprecated}}</span>,
</span>
{% endif %}
<span class="tag">
<span>Auth:</span>
<span>{% if item.auth %}YES{% else %}NO{% endif %}</span>
</span>
</div>
</div>
<div class="rpc-row-detail hidden">

View File

@@ -2,7 +2,7 @@
export PENPOT_HOST=devenv
export PENPOT_TENANT=dev
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp enable-webhooks"
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp"
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot"
# export PENPOT_DATABASE_USERNAME="penpot"

View File

@@ -2,7 +2,7 @@
export PENPOT_HOST=devenv
export PENPOT_TENANT=dev
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp enable-webhooks"
export PENPOT_FLAGS="$PENPOT_FLAGS enable-backend-asserts enable-audit-log enable-transit-readable-response enable-demo-users disable-secure-session-cookies enable-smtp"
set -ex

View File

@@ -1,26 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.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,9 +106,6 @@
(s/def ::file-change-snapshot-every ::us/integer)
(s/def ::file-change-snapshot-timeout ::dt/duration)
(s/def ::setup-admin-email ::us/email)
(s/def ::setup-admin-password ::us/not-empty-string)
(s/def ::default-executor-parallelism ::us/integer)
(s/def ::scheduled-executor-parallelism ::us/integer)
@@ -298,9 +295,6 @@
::srepl-host
::srepl-port
::setup-admin-email
::setup-admin-password
::assets-storage-backend
::storage-assets-fs-directory
::storage-assets-s3-bucket

View File

@@ -11,8 +11,7 @@
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[java-http-clj.core :as http]
[promesa.core :as p])
[java-http-clj.core :as http])
(:import
java.net.http.HttpClient))
@@ -35,10 +34,7 @@
(us/assert! ::client client)
(if sync?
(http/send req {:client client :as response-type})
(try
(http/send-async req {:client client :as response-type})
(catch Throwable cause
(p/rejected cause))))))
(http/send-async req {:client client :as response-type}))))
(defn req!
"A convencience toplevel function for gradual migration to a new API

View File

@@ -78,12 +78,13 @@
(raise cause)))]
(fn [request respond raise]
(let [request (ex/try! (process-request request))]
(if (ex/exception? request)
(if (instance? RuntimeException request)
(handle-error raise (or (ex/cause request) request))
(handle-error raise request))
(handler request respond raise))))))
(when-let [request (try
(process-request request)
(catch RuntimeException cause
(handle-error raise (or (.getCause cause) cause)))
(catch Throwable cause
(handle-error raise cause)))]
(handler request respond raise)))))
(def parse-request
{:name ::parse-request

View File

@@ -12,7 +12,6 @@
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.main :as-alias main]
[app.tokens :as tokens]
[app.util.time :as dt]
[app.worker :as wrk]
@@ -57,13 +56,13 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- prepare-session-params
[props data]
[sprops data]
(let [profile-id (:profile-id data)
user-agent (:user-agent data)
created-at (or (:created-at data) (dt/now))
token (tokens/generate props {:iss "authentication"
:iat created-at
:uid profile-id})]
token (tokens/generate sprops {:iss "authentication"
:iat created-at
:uid profile-id})]
{:user-agent user-agent
:profile-id profile-id
:created-at created-at
@@ -71,7 +70,7 @@
:id token}))
(defn- database-manager
[{:keys [::db/pool ::wrk/executor ::main/props]}]
[{:keys [pool sprops executor]}]
(reify ISessionManager
(read [_ token]
(px/with-dispatch executor
@@ -79,11 +78,11 @@
(decode [_ token]
(px/with-dispatch executor
(tokens/verify props {:token token :iss "authentication"})))
(tokens/verify sprops {:token token :iss "authentication"})))
(write! [_ _ data]
(px/with-dispatch executor
(let [params (prepare-session-params props data)]
(let [params (prepare-session-params sprops data)]
(db/insert! pool :http-session params)
params)))
@@ -101,7 +100,7 @@
nil))))
(defn inmemory-manager
[{:keys [::wrk/executor ::main/props]}]
[{:keys [sprops executor]}]
(let [cache (atom {})]
(reify ISessionManager
(read [_ token]
@@ -109,11 +108,11 @@
(decode [_ token]
(px/with-dispatch executor
(tokens/verify props {:token token :iss "authentication"})))
(tokens/verify sprops {:token token :iss "authentication"})))
(write! [_ _ data]
(p/do
(let [{:keys [token] :as params} (prepare-session-params props data)]
(let [{:keys [token] :as params} (prepare-session-params sprops data)]
(swap! cache assoc token params)
params)))
@@ -128,11 +127,12 @@
(swap! cache dissoc token)
nil)))))
(s/def ::sprops map?)
(defmethod ig/pre-init-spec ::manager [_]
(s/keys :req [::db/pool ::wrk/executor ::main/props]))
(s/keys :req-un [::db/pool ::wrk/executor ::sprops]))
(defmethod ig/init-key ::manager
[_ {:keys [::db/pool] :as cfg}]
[_ {:keys [pool] :as cfg}]
(if (db/read-only? pool)
(inmemory-manager cfg)
(database-manager cfg)))
@@ -178,19 +178,18 @@
(clear-authenticated-cookie))))))
(def middleware-1
(letfn [(decode-cookie [manager cookie]
(if-let [value (:value cookie)]
(decode manager value)
(p/resolved nil)))
(letfn [(wrap-handler [manager handler request respond raise]
(try
(let [claims (some->> (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
(yrq/get-cookie request)
(decode manager))
request (cond-> request
(some? claims)
(assoc :session-token-claims claims))]
(handler request respond raise))
(catch Throwable _
(handler request respond raise))))]
(wrap-handler [manager handler request respond raise]
(let [cookie (some->> (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
(yrq/get-cookie request))]
(->> (decode-cookie manager cookie)
(p/fnly (fn [claims _]
(cond-> request
(some? claims) (assoc :session-token-claims claims)
:always (handler respond raise)))))))]
{:name :session-1
:compile (fn [& _]
(fn [handler manager]

View File

@@ -21,7 +21,6 @@
[app.main :as-alias main]
[app.metrics :as mtx]
[app.tokens :as tokens]
[app.util.retry :as rtry]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
@@ -148,46 +147,35 @@
:name (:name event)
:type (:type event)
:profile-id (:profile-id event)
:tracked-at (dt/now)
:ip-addr (:ip-addr event)
:props (:props event)}]
(when (contains? cf/flags :audit-log)
;; NOTE: this operation may cause primary key conflicts on inserts
;; because of the timestamp precission (two concurrent requests), in
;; this case we just retry the operation.
(rtry/with-retry {::rtry/when rtry/conflict-exception?
::rtry/max-retries 6
::rtry/label "persist-audit-log-event"}
(let [now (dt/now)]
(db/insert! pool :audit-log
(-> params
(update :props db/tjson)
(update :ip-addr db/inet)
(assoc :created-at now)
(assoc :tracked-at now)
(assoc :source "backend"))))))
(db/insert! pool :audit-log
(-> params
(update :props db/tjson)
(update :ip-addr db/inet)
(assoc :source "backend"))))
(when (and (contains? cf/flags :webhooks)
(::webhooks/event? event))
(let [batch-key (::webhooks/batch-key event)
batch-timeout (::webhooks/batch-timeout event)
label-suffix (when (ifn? batch-key)
(str/ffmt ":%" (batch-key (:props params))))
dedupe? (boolean
(and batch-key batch-timeout))]
batch-timeout (::webhooks/batch-timeout event)]
(wrk/submit! ::wrk/conn pool
::wrk/task :process-webhook-event
::wrk/queue :webhooks
::wrk/max-retries 0
::wrk/delay (or batch-timeout 0)
::wrk/dedupe dedupe?
::wrk/label
(str/ffmt "rpc:%1%2" (:name params) label-suffix)
::webhooks/event
(-> params
(dissoc :ip-addr)
(dissoc :type)))))))
::wrk/label (cond
(fn? batch-key) (batch-key (:props event))
(keyword? batch-key) (name batch-key)
(string? batch-key) batch-key
:else "default")
::wrk/dedupe true
::webhooks/event (-> params
(dissoc :ip-addr)
(dissoc :type)))))))
(defn submit!
"Submit audit event to the collector."

View File

@@ -25,11 +25,11 @@
(defn- lookup-webhooks-by-team
[pool team-id]
(db/exec! pool ["select w.* from webhook as w where team_id=? and is_active=true" team-id]))
(db/exec! pool ["select * from webhook where team_id=? and is_active=true" team-id]))
(defn- lookup-webhooks-by-project
[pool project-id]
(let [sql [(str "select w.* from webhook as w"
(let [sql [(str "select * from webhook as w"
" join project as p on (p.team_id = w.team_id)"
" where p.id = ? and w.is_active = true")
project-id]]
@@ -37,7 +37,7 @@
(defn- lookup-webhooks-by-file
[pool file-id]
(let [sql [(str "select w.* from webhook as w"
(let [sql [(str "select * from webhook as w"
" join project as p on (p.team_id = w.team_id)"
" join file as f on (f.project_id = p.id)"
" where f.id = ? and w.is_active = true")
@@ -62,6 +62,7 @@
:name (:name event))
(when-let [items (lookup-webhooks cfg event)]
;; (app.common.pprint/pprint items)
(l/trace :hint "webhooks found for event" :total (count items))
(db/with-atomic [conn pool]
@@ -168,9 +169,6 @@
(instance? java.net.ConnectException cause)
"connection-error"
(instance? java.lang.IllegalArgumentException cause)
"invalid-uri"
(instance? java.net.http.HttpConnectTimeoutException cause)
"timeout"
))

View File

@@ -207,9 +207,9 @@
{::wrk/executor (ig/ref ::wrk/executor)}
:app.http.session/manager
{::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref :app.setup/props)}
{:pool (ig/ref ::db/pool)
:sprops (ig/ref :app.setup/props)
:executor (ig/ref ::wrk/executor)}
:app.http.session/gc-task
{:pool (ig/ref ::db/pool)
@@ -290,11 +290,7 @@
{:pool (ig/ref ::db/pool)
:executor (ig/ref ::wrk/executor)
:storage (ig/ref ::sto/storage)
:session (ig/ref :app.http.session/manager)
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::sto/storage (ig/ref ::sto/storage)}
:session (ig/ref :app.http.session/manager)}
:app.http.websocket/handler
{:pool (ig/ref ::db/pool)
@@ -326,7 +322,7 @@
::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)
::wrk/executor (ig/ref ::wrk/executor)
::props (ig/ref :app.setup/props)
:pool (ig/ref ::db/pool)
:session (ig/ref :app.http.session/manager)
:sprops (ig/ref :app.setup/props)
@@ -389,8 +385,8 @@
:max-age cf/deletion-delay}
:app.tasks.objects-gc/handler
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
{:pool (ig/ref ::db/pool)
:storage (ig/ref ::sto/storage)}
:app.tasks.file-gc/handler
{:pool (ig/ref ::db/pool)}
@@ -407,9 +403,6 @@
{:port (cf/get :srepl-port)
:host (cf/get :srepl-host)}
:app.setup/initial-profile
{::db/pool (ig/ref ::db/pool)}
:app.setup/builtin-templates
{::http.client/client (ig/ref ::http.client/client)}

View File

@@ -132,7 +132,7 @@
(defmethod run-collector! :counter
[{:keys [::mdef/instance]} {:keys [inc labels] :or {inc 1 labels default-empty-labels}}]
(let [instance (.labels ^Counter instance (if (is-array? labels) labels (into-array String labels)))]
(let [instance (.labels instance (if (is-array? labels) labels (into-array String labels)))]
(.inc ^Counter$Child instance (double inc))))
(defmethod run-collector! :gauge

View File

@@ -271,37 +271,6 @@
{:name "0087-mod-task-table"
:fn (mg/resource "app/migrations/sql/0087-mod-task-table.sql")}
{:name "0088-mod-team-profile-rel-table"
:fn (mg/resource "app/migrations/sql/0088-mod-team-profile-rel-table.sql")}
{:name "0089-mod-project-profile-rel-table"
:fn (mg/resource "app/migrations/sql/0089-mod-project-profile-rel-table.sql")}
{:name "0090-mod-http-session-table"
:fn (mg/resource "app/migrations/sql/0090-mod-http-session-table.sql")}
{:name "0091-mod-team-project-profile-rel-table"
:fn (mg/resource "app/migrations/sql/0091-mod-team-project-profile-rel-table.sql")}
{:name "0092-mod-team-invitation-table"
:fn (mg/resource "app/migrations/sql/0092-mod-team-invitation-table.sql")}
{:name "0093-del-file-share-tokens-table"
:fn (mg/resource "app/migrations/sql/0093-del-file-share-tokens-table.sql")}
{:name "0094-del-profile-attr-table"
:fn (mg/resource "app/migrations/sql/0094-del-profile-attr-table.sql")}
{:name "0095-del-storage-data-table"
:fn (mg/resource "app/migrations/sql/0095-del-storage-data-table.sql")}
{:name "0096-del-storage-pending-table"
:fn (mg/resource "app/migrations/sql/0096-del-storage-pending-table.sql")}
{:name "0097-mod-profile-table"
:fn (mg/resource "app/migrations/sql/0097-mod-profile-table.sql")}
])

View File

@@ -1,3 +0,0 @@
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

@@ -1,3 +0,0 @@
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

@@ -1,2 +0,0 @@
ALTER TABLE http_session DROP CONSTRAINT http_session_pkey;
ALTER TABLE http_session ADD CONSTRAINT http_session_pkey PRIMARY KEY (id);

View File

@@ -1,3 +0,0 @@
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

@@ -1,3 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,8 +35,6 @@
[yetti.request :as yrq]
[yetti.response :as yrs]))
(s/def ::profile-id ::us/uuid)
(defn- default-handler
[_]
(p/rejected (ex/error :type :not-found)))
@@ -74,11 +72,8 @@
(let [type (keyword (:type params))
data (into {::http/request request} params)
data (if profile-id
(assoc data
:profile-id profile-id
::profile-id profile-id
::session-id session-id)
(dissoc data :profile-id ::profile-id))
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
method (get methods type default-handler)]
(-> (method data)
@@ -95,11 +90,8 @@
(let [type (keyword (:type params))
data (into {::http/request request} params)
data (if profile-id
(assoc data
:profile-id profile-id
::profile-id profile-id
::session-id session-id)
(dissoc data :profile-id ::profile-id))
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
method (get methods type default-handler)]
(-> (method data)
@@ -113,12 +105,13 @@
"Ring handler that dispatches cmd requests and convert between
internal async flow into ring async flow."
[methods {:keys [profile-id session-id params] :as request} respond raise]
(let [cmd (keyword (:type params))
(let [cmd (keyword (:command params))
etag (yrq/get-header request "if-none-match")
data (into {::http/request request ::cond/key etag} params)
data (if profile-id
(assoc data ::profile-id profile-id ::session-id session-id)
(dissoc data ::profile-id))
(assoc data :profile-id profile-id ::session-id session-id)
(dissoc data :profile-id))
method (get methods cmd default-handler)]
(binding [cond/*enabled* true]
(-> (method data)
@@ -159,21 +152,17 @@
(letfn [(handle-audit [params result]
(let [resultm (meta result)
request (::http/request params)
profile-id (or (::audit/profile-id resultm)
(:profile-id result)
(if (= (::type cfg) "command")
(::profile-id params)
(:profile-id params))
(:profile-id params)
uuid/zero)
props (-> (or (::audit/replace-props resultm)
(-> params
(merge (::audit/props resultm))
(dissoc :profile-id)
(dissoc :type)))
(d/without-qualified)
(d/without-nils))
props (or (::audit/replace-props resultm)
(-> params
(d/without-qualified)
(merge (::audit/props resultm))
(dissoc :profile-id)
(dissoc :type)))
event {:type (or (::audit/type resultm)
(::type cfg))
@@ -219,24 +208,21 @@
(wrap-audit cfg $ mdata))
spec (or (::sv/spec mdata) (s/spec any?))
auth? (::auth mdata true)]
auth? (:auth mdata true)]
(l/debug :hint "register method" :name (::sv/name mdata))
(with-meta
(fn [params]
(fn [{:keys [::request] :as params}]
;; Raise authentication error when rpc method requires auth but
;; no profile-id is found in the request.
(let [profile-id (if (= "command" (::type cfg))
(::profile-id params)
(:profile-id params))]
(p/do!
(if (and auth? (not (uuid? profile-id)))
(ex/raise :type :authentication
:code :authentication-required
:hint "authentication required for this endpoint")
(let [params (us/conform spec params)]
(f cfg params))))))
(p/do!
(if (and auth? (not (uuid? (:profile-id params))))
(ex/raise :type :authentication
:code :authentication-required
:hint "authentication required for this endpoint")
(let [params (us/conform spec (dissoc params ::request))]
(f cfg (assoc params ::request request))))))
mdata)))
(defn- process-method
@@ -251,6 +237,7 @@
(->> (sv/scan-ns 'app.rpc.queries.projects
'app.rpc.queries.files
'app.rpc.queries.teams
'app.rpc.queries.comments
'app.rpc.queries.profile
'app.rpc.queries.viewer
'app.rpc.queries.fonts)
@@ -263,10 +250,13 @@
(->> (sv/scan-ns 'app.rpc.mutations.media
'app.rpc.mutations.profile
'app.rpc.mutations.files
'app.rpc.mutations.comments
'app.rpc.mutations.projects
'app.rpc.mutations.teams
'app.rpc.mutations.management
'app.rpc.mutations.fonts
'app.rpc.mutations.share-link)
'app.rpc.mutations.share-link
'app.rpc.mutations.verify-token)
(map (partial process-method cfg))
(into {}))))
@@ -275,11 +265,9 @@
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
(->> (sv/scan-ns 'app.rpc.commands.binfile
'app.rpc.commands.comments
'app.rpc.commands.profile
'app.rpc.commands.management
'app.rpc.commands.verify-token
'app.rpc.commands.search
'app.rpc.commands.teams
'app.rpc.commands.auth
'app.rpc.commands.ldap
'app.rpc.commands.demo
@@ -343,7 +331,7 @@
(defmethod ig/init-key ::routes
[_ {:keys [methods] :as cfg}]
[["/rpc"
["/command/:type" {:handler (partial rpc-command-handler (:commands methods))}]
["/command/:command" {:handler (partial rpc-command-handler (:commands methods))}]
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
:allowed-methods #{:post}}]]])

View File

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

View File

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

View File

@@ -15,13 +15,9 @@
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.projects :as projects]
[app.storage :as sto]
[app.storage.tmp :as tmp]
@@ -294,7 +290,7 @@
(defn- retrieve-file
[pool file-id]
(with-open [^AutoCloseable conn (db/open pool)]
(with-open [conn (db/open pool)]
(binding [pmap/*load-fn* (partial files/load-pointer conn file-id)]
(some-> (db/get* conn :file {:id file-id})
(files/decode-row)
@@ -844,10 +840,10 @@
(defn import!
[{:keys [::input] :as cfg}]
(let [id (uuid/next)
tp (dt/tpoint)
ts (dt/now)
cs (volatile! nil)]
(l/info :hint "import: started" :import-id id)
(try
(l/info :hint "start importation" :import-id id)
(binding [*position* (atom 0)]
(with-open [^AutoCloseable input (io/input-stream input)]
(read-import! (assoc cfg ::input input))))
@@ -857,27 +853,25 @@
(throw cause))
(finally
(l/info :hint "import: terminated"
:import-id id
:elapsed (dt/format-duration (tp))
(l/info :hint "importation finished" :import-id id
:elapsed (str (inst-ms (dt/diff ts (dt/now))) "ms")
:error? (some? @cs)
:cause @cs
)))))
:cause @cs)))))
;; --- Command: export-binfile
(s/def ::file-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::include-libraries? ::us/boolean)
(s/def ::embed-assets? ::us/boolean)
(s/def ::export-binfile
(s/keys :req [::rpc/profile-id] :req-un [::file-id ::include-libraries? ::embed-assets?]))
(s/keys :req-un [::profile-id ::file-id ::include-libraries? ::embed-assets?]))
(sv/defmethod ::export-binfile
"Export a penpot file in a binary format."
{::doc/added "1.15"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id include-libraries? embed-assets?] :as params}]
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id include-libraries? embed-assets?] :as params}]
(files/check-read-permissions! pool profile-id file-id)
(let [body (reify yrs/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream]
@@ -892,18 +886,15 @@
(s/def ::file ::media/upload)
(s/def ::import-binfile
(s/keys :req [::rpc/profile-id] :req-un [::project-id ::file]))
(s/keys :req-un [::profile-id ::project-id ::file]))
(sv/defmethod ::import-binfile
"Import a penpot file in a binary format."
{::doc/added "1.15"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id project-id file] :as params}]
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id project-id file] :as params}]
(db/with-atomic [conn pool]
(projects/check-read-permissions! conn profile-id project-id)
(let [ids (import! (assoc cfg
::input (:path file)
::project-id project-id
::ignore-index-errors? true))]
(rph/with-meta ids
{::audit/props {:file nil :file-ids ids}}))))
(import! (assoc cfg
::input (:path file)
::project-id project-id
::ignore-index-errors? true))))

View File

@@ -10,15 +10,12 @@
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.teams :as teams]
[app.rpc.retry :as retry]
[app.util.blob :as blob]
[app.util.retry :as rtry]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -42,7 +39,7 @@
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-comment-threads
(s/and (s/keys :req [::rpc/profile-id]
(s/and (s/keys :req-un [::profile-id]
:opt-un [::file-id ::share-id ::team-id])
#(or (:file-id %) (:team-id %))))
@@ -75,7 +72,7 @@
window w as (partition by c.thread_id order by c.created_at asc)")
(defn retrieve-comment-threads
[conn {:keys [::rpc/profile-id file-id share-id]}]
[conn {:keys [profile-id file-id share-id]}]
(files/check-comment-permissions! conn profile-id file-id share-id)
(->> (db/exec! conn [sql:comment-threads profile-id file-id])
(into [] (map decode-row))))
@@ -86,12 +83,10 @@
(s/def ::team-id ::us/uuid)
(s/def ::get-unread-comment-threads
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::get-unread-comment-threads
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(retrieve-unread-comment-threads conn params)))
@@ -124,7 +119,7 @@
"select * from threads where count_unread_comments > 0"))
(defn retrieve-unread-comment-threads
[conn {:keys [::rpc/profile-id team-id]}]
[conn {:keys [profile-id team-id]}]
(->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id])
(into [] (map decode-row))))
@@ -134,13 +129,11 @@
(s/def ::id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-comment-thread
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::id]
(s/keys :req-un [::profile-id ::file-id ::id]
:opt-un [::share-id]))
(sv/defmethod ::get-comment-thread
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id id share-id] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id file-id id share-id] :as params}]
(with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(let [sql (str "with threads as (" sql:comment-threads ")"
@@ -149,7 +142,7 @@
(decode-row)))))
(defn get-comment-thread
[conn {:keys [::rpc/profile-id file-id id] :as params}]
[conn {:keys [profile-id file-id id] :as params}]
(let [sql (str "with threads as (" sql:comment-threads ")"
"select * from threads where id = ?")]
(-> (db/exec-one! conn [sql profile-id file-id id])
@@ -163,13 +156,11 @@
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::thread-id ::us/uuid)
(s/def ::get-comments
(s/keys :req [::rpc/profile-id]
:req-un [::thread-id]
(s/keys :req-un [::profile-id ::thread-id]
:opt-un [::share-id]))
(sv/defmethod ::get-comments
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id thread-id share-id] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id thread-id share-id] :as params}]
(with-open [conn (db/open pool)]
(let [thread (db/get-by-id conn :comment-thread thread-id)]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id))
@@ -195,8 +186,7 @@
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::get-profiles-for-file-comments
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::share-id]))
(sv/defmethod ::get-profiles-for-file-comments
@@ -204,7 +194,7 @@
participants on comment threads of the file."
{::doc/added "1.15"
::doc/changes ["1.15" "Imported from queries and renamed."]}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id]}]
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id]}]
(with-open [conn (db/open pool)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(get-file-comments-users conn file-id profile-id)))
@@ -245,26 +235,24 @@
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::profile-id ::us/uuid)
(s/def ::position ::gpt/point)
(s/def ::content ::us/string)
(s/def ::frame-id ::us/uuid)
(s/def ::create-comment-thread
(s/keys :req [::rpc/profile-id]
:req-un [::file-id ::position ::content ::page-id ::frame-id]
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id ::frame-id]
:opt-un [::share-id]))
(sv/defmethod ::create-comment-thread
{::doc/added "1.15"
{::retry/max-retries 3
::retry/matches retry/conflict-db-insert?
::doc/added "1.15"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id share-id] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
(db/with-atomic [conn pool]
(files/check-comment-permissions! conn profile-id file-id share-id)
(rtry/with-retry {::rtry/when rtry/conflict-exception?
::rtry/max-retries 3
::rtry/label "create-comment-thread"}
(create-comment-thread conn params))))
(create-comment-thread conn params)))
(defn- retrieve-next-seqn
[conn file-id]
@@ -273,7 +261,7 @@
(:next-seqn res)))
(defn create-comment-thread
[conn {:keys [::rpc/profile-id file-id page-id position content frame-id] :as params}]
[conn {:keys [profile-id file-id page-id position content frame-id] :as params}]
(let [seqn (retrieve-next-seqn conn file-id)
now (dt/now)
pname (retrieve-page-name conn params)
@@ -321,13 +309,12 @@
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::update-comment-thread-status
(s/keys :req [::rpc/profile-id]
:req-un [::id]
(s/keys :req-un [::profile-id ::id]
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-status
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id share-id] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not cthr
@@ -352,13 +339,12 @@
(s/def ::is-resolved ::us/boolean)
(s/def ::update-comment-thread
(s/keys :req [::rpc/profile-id]
:req-un [::id ::is-resolved]
(s/keys :req-un [::profile-id ::id ::is-resolved]
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id is-resolved share-id] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id id is-resolved share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not thread
@@ -377,8 +363,7 @@
(declare create-comment)
(s/def ::create-comment
(s/keys :req [::rpc/profile-id]
:req-un [::thread-id ::content]
(s/keys :req-un [::profile-id ::thread-id ::content]
:opt-un [::share-id]))
(sv/defmethod ::create-comment
@@ -389,7 +374,7 @@
(create-comment conn params)))
(defn create-comment
[conn {:keys [::rpc/profile-id thread-id content share-id] :as params}]
[conn {:keys [profile-id thread-id content share-id] :as params}]
(let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true})
(decode-row))
pname (retrieve-page-name conn thread)]
@@ -436,17 +421,14 @@
(upsert-comment-thread-status! conn profile-id thread-id)
;; Return the created comment object.
(rph/with-meta comment
{::audit/props {:file-id (:file-id thread)
:share-id nil}}))))
comment)))
;; --- COMMAND: Update Comment
(declare update-comment)
(s/def ::update-comment
(s/keys :req [::rpc/profile-id]
:req-un [::id ::content]
(s/keys :req-un [::profile-id ::id ::content]
:opt-un [::share-id]))
(sv/defmethod ::update-comment
@@ -456,7 +438,7 @@
(update-comment conn params)))
(defn update-comment
[conn {:keys [::rpc/profile-id id content share-id] :as params}]
[conn {:keys [profile-id id content share-id] :as params}]
(let [comment (db/get-by-id conn :comment id {:for-update true})
_ (when-not comment (ex/raise :type :not-found))
thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true})
@@ -485,12 +467,11 @@
;; --- COMMAND: Delete Comment Thread
(s/def ::delete-comment-thread
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-comment-thread
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not (= (:owner-id thread) profile-id)
@@ -503,12 +484,12 @@
;; --- COMMAND: Delete comment
(s/def ::delete-comment
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-comment
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
{::doc/added "1.15"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})]
(when-not (= (:owner-id comment) profile-id)
@@ -520,13 +501,12 @@
;; --- COMMAND: Update comment thread position
(s/def ::update-comment-thread-position
(s/keys :req [::rpc/profile-id]
:req-un [::id ::position ::frame-id]
(s/keys :req-un [::profile-id ::id ::position ::frame-id]
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-position
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id position frame-id share-id] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id id position frame-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
@@ -540,13 +520,12 @@
;; --- COMMAND: Update comment frame
(s/def ::update-comment-thread-frame
(s/keys :req [::rpc/profile-id]
:req-un [::id ::frame-id]
(s/keys :req-un [::profile-id ::id ::frame-id]
:opt-un [::share-id]))
(sv/defmethod ::update-comment-thread-frame
{::doc/added "1.15"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id frame-id share-id] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id id frame-id share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(files/check-comment-permissions! conn profile-id (:file-id thread) share-id)

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,6 @@
[app.loggers.webhooks :as-alias webhooks]
[app.metrics :as mtx]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.files :as files]
[app.rpc.doc :as-alias doc]
@@ -53,8 +52,7 @@
(s/def ::revn ::us/integer)
(s/def ::update-file
(s/and
(s/keys :req [::rpc/profile-id]
:req-un [::files/id ::session-id ::revn]
(s/keys :req-un [::files/id ::files/profile-id ::session-id ::revn]
:opt-un [::changes ::changes-with-metadata ::features])
(fn [o]
(or (contains? o :changes)
@@ -125,27 +123,30 @@
;; set is different than the persisted one, update it on the
;; database.
(defn webhook-batch-keyfn
[props]
(str "rpc:update-file:" (:id props)))
(sv/defmethod ::update-file
{::climit/queue :update-file
::climit/key-fn :id
::webhooks/event? true
::webhooks/batch-timeout (dt/duration "2m")
::webhooks/batch-key :id
::webhooks/batch-timeout (dt/duration "2s")
::webhooks/batch-key webhook-batch-keyfn
::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id)
(let [cfg (assoc cfg :conn conn)
params (assoc params :profile-id profile-id)
tpoint (dt/tpoint)]
(-> (update-file cfg params)
(rph/with-defer #(let [elapsed (tpoint)]
(l/trace :hint "update-file" :time (dt/format-duration elapsed))))))))
(defn update-file
[{:keys [conn metrics] :as cfg} {:keys [profile-id id changes changes-with-metadata] :as params}]
[{:keys [conn metrics] :as cfg} {:keys [id profile-id changes changes-with-metadata] :as params}]
(let [file (get-file conn id)
features (->> (concat (:features file)
(:features params))
@@ -187,7 +188,7 @@
:team-id (:team-id file)}))))))
(defn- update-file*
[{:keys [conn] :as cfg} {:keys [profile-id file changes session-id] :as params}]
[{:keys [conn] :as cfg} {:keys [file changes session-id profile-id] :as params}]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation

View File

@@ -13,13 +13,12 @@
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.binfile :as binfile]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams :refer [create-project-role create-project]]
[app.rpc.doc :as-alias doc]
[app.rpc.mutations.projects :refer [create-project-role create-project]]
[app.rpc.queries.projects :as proj]
[app.rpc.queries.teams :as teams]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
@@ -32,23 +31,22 @@
(declare duplicate-file)
(s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::duplicate-file
(s/keys :req [::rpc/profile-id]
:req-un [::file-id]
(s/keys :req-un [::profile-id ::file-id]
:opt-un [::name]))
(sv/defmethod ::duplicate-file
"Duplicate a single file in the same team."
{::doc/added "1.16"
::webhooks/event? true}
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(duplicate-file conn (assoc params :profile-id (::rpc/profile-id params)))))
(duplicate-file conn params)))
(defn- remap-id
[item index key]
@@ -213,17 +211,15 @@
(declare duplicate-project)
(s/def ::duplicate-project
(s/keys :req [::rpc/profile-id]
:req-un [::project-id]
(s/keys :req-un [::profile-id ::project-id]
:opt-un [::name]))
(sv/defmethod ::duplicate-project
"Duplicate an entire project with all the files"
{::doc/added "1.16"
::webhooks/event? true}
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(duplicate-project conn (assoc params :profile-id (::rpc/profile-id params)))))
(duplicate-project conn params)))
(defn duplicate-project
[conn {:keys [profile-id project-id name] :as params}]
@@ -251,7 +247,9 @@
;; create the duplicated project and assign the current profile as
;; a project owner
(create-project conn project)
(create-project-role conn profile-id (:id project) :owner)
(create-project-role conn {:project-id (:id project)
:profile-id profile-id
:role :owner})
;; duplicate all files
(let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} files)
@@ -322,16 +320,14 @@
(s/def ::ids (s/every ::us/uuid :kind set?))
(s/def ::move-files
(s/keys :req [::rpc/profile-id]
:req-un [::ids ::project-id]))
(s/keys :req-un [::profile-id ::ids ::project-id]))
(sv/defmethod ::move-files
"Move a set of files from one project to other."
{::doc/added "1.16"
::webhooks/event? true}
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(move-files conn (assoc params :profile-id (::rpc/profile-id params)))))
(move-files conn params)))
;; --- COMMAND: Move project
@@ -363,16 +359,14 @@
(s/def ::move-project
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::project-id]))
(s/keys :req-un [::profile-id ::team-id ::project-id]))
(sv/defmethod ::move-project
"Move projects between teams."
{::doc/added "1.16"
::webhooks/event? true}
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(move-project conn (assoc params :profile-id (::rpc/profile-id params)))))
(move-project conn params)))
;; --- COMMAND: Clone Template
@@ -380,17 +374,15 @@
(s/def ::template-id ::us/not-empty-string)
(s/def ::clone-template
(s/keys :req [::rpc/profile-id]
:req-un [::project-id ::template-id]))
(s/keys :req-un [::profile-id ::project-id ::template-id]))
(sv/defmethod ::clone-template
"Clone into the specified project the template by its id."
{::doc/added "1.16"
::webhooks/event? true}
{::doc/added "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(clone-template (assoc params :profile-id (::rpc/profile-id params))))))
(clone-template params))))
(defn- clone-template
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]

View File

@@ -1,75 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.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,7 +8,6 @@
(:require
[app.common.spec :as us]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
@@ -48,18 +47,18 @@
order by f.created_at asc")
(defn search-files
[conn {:keys [::rpc/profile-id team-id search-term] :as params}]
[conn {:keys [profile-id team-id search-term] :as params}]
(db/exec! conn [sql:search-files
profile-id team-id
profile-id team-id
search-term]))
(s/def ::profile-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::search-files ::us/string)
(s/def ::search-files
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]
(s/keys :req-un [::profile-id ::team-id]
:opt-un [::search-term]))
(sv/defmethod ::search-files

View File

@@ -1,863 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.commands.teams
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.permissions :as perms]
[app.rpc.queries.profile :as profile]
[app.storage :as sto]
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[promesa.core :as p]
[promesa.exec :as px]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::file-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(def ^:private sql:team-permissions
"select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
join team as t on (t.id = tpr.team_id)
where tpr.profile_id = ?
and tpr.team_id = ?
and t.deleted_at is null")
(defn get-permissions
[conn profile-id team-id]
(let [rows (db/exec! conn [sql:team-permissions profile-id team-id])
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true})))
(def has-edit-permissions?
(perms/make-edition-predicate-fn get-permissions))
(def has-read-permissions?
(perms/make-read-predicate-fn get-permissions))
(def check-edition-permissions!
(perms/make-check-fn has-edit-permissions?))
(def check-read-permissions!
(perms/make-check-fn has-read-permissions?))
;; --- Query: Teams
(declare retrieve-teams)
(s/def ::get-teams
(s/keys :req [::rpc/profile-id]))
(sv/defmethod ::get-teams
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(with-open [conn (db/open pool)]
(retrieve-teams conn profile-id)))
(def sql:teams
"select t.*,
tp.is_owner,
tp.is_admin,
tp.can_edit,
(t.id = ?) as is_default
from team_profile_rel as tp
join team as t on (t.id = tp.team_id)
where t.deleted_at is null
and tp.profile_id = ?
order by tp.created_at asc")
(defn process-permissions
[team]
(let [is-owner (:is-owner team)
is-admin (:is-admin team)
can-edit (:can-edit team)
permissions {:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)}]
(-> team
(dissoc :is-owner :is-admin :can-edit)
(assoc :permissions permissions))))
(defn retrieve-teams
[conn profile-id]
(let [defaults (profile/retrieve-additional-data conn profile-id)]
(->> (db/exec! conn [sql:teams (:default-team-id defaults) profile-id])
(mapv process-permissions))))
;; --- Query: Team (by ID)
(declare retrieve-team)
(s/def ::get-team
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
(sv/defmethod ::get-team
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id]}]
(with-open [conn (db/open pool)]
(retrieve-team conn profile-id id)))
(defn retrieve-team
[conn profile-id team-id]
(let [defaults (profile/retrieve-additional-data conn profile-id)
sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?")
result (db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id])]
(when-not result
(ex/raise :type :not-found
:code :team-does-not-exist))
(process-permissions result)))
;; --- Query: Team Members
(def sql:team-members
"select tp.*,
p.id,
p.email,
p.fullname as name,
p.fullname as fullname,
p.photo_id,
p.is_active
from team_profile_rel as tp
join profile as p on (p.id = tp.profile_id)
where tp.team_id = ?")
(defn retrieve-team-members
[conn team-id]
(db/exec! conn [sql:team-members team-id]))
(s/def ::team-id ::us/uuid)
(s/def ::get-team-members
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(sv/defmethod ::get-team-members
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(retrieve-team-members conn team-id)))
;; --- Query: Team Users
(declare retrieve-users)
(declare retrieve-team-for-file)
(s/def ::get-team-users
(s/and (s/keys :req [::rpc/profile-id]
:opt-un [::team-id ::file-id])
#(or (:team-id %) (:file-id %))))
(sv/defmethod ::get-team-users
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id file-id]}]
(with-open [conn (db/open pool)]
(if team-id
(do
(check-read-permissions! conn profile-id team-id)
(retrieve-users conn team-id))
(let [{team-id :id} (retrieve-team-for-file conn file-id)]
(check-read-permissions! conn profile-id team-id)
(retrieve-users conn team-id)))))
;; This is a similar query to team members but can contain more data
;; because some user can be explicitly added to project or file (not
;; implemented in UI)
(def sql:team-users
"select pf.id, pf.fullname, pf.photo_id
from profile as pf
inner join team_profile_rel as tpr on (tpr.profile_id = pf.id)
where tpr.team_id = ?
union
select pf.id, pf.fullname, pf.photo_id
from profile as pf
inner join project_profile_rel as ppr on (ppr.profile_id = pf.id)
inner join project as p on (ppr.project_id = p.id)
where p.team_id = ?
union
select pf.id, pf.fullname, pf.photo_id
from profile as pf
inner join file_profile_rel as fpr on (fpr.profile_id = pf.id)
inner join file as f on (fpr.file_id = f.id)
inner join project as p on (f.project_id = p.id)
where p.team_id = ?")
(def sql:team-by-file
"select p.team_id as id
from project as p
join file as f on (p.id = f.project_id)
where f.id = ?")
(defn retrieve-users
[conn team-id]
(db/exec! conn [sql:team-users team-id team-id team-id]))
(defn retrieve-team-for-file
[conn file-id]
(->> [sql:team-by-file file-id]
(db/exec-one! conn)))
;; --- Query: Team Stats
(declare retrieve-team-stats)
(s/def ::get-team-stats
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(sv/defmethod ::get-team-stats
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(retrieve-team-stats conn team-id)))
(def sql:team-stats
"select (select count(*) from project where team_id = ?) as projects,
(select count(*) from file as f join project as p on (p.id = f.project_id) where p.team_id = ?) as files")
(defn retrieve-team-stats
[conn team-id]
(db/exec-one! conn [sql:team-stats team-id team-id]))
;; --- Query: Team invitations
(s/def ::get-team-invitations
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(def sql:team-invitations
"select email_to as email, role, (valid_until < now()) as expired
from team_invitation where team_id = ? order by valid_until desc, created_at desc")
(defn get-team-invitations
[conn team-id]
(->> (db/exec! conn [sql:team-invitations team-id])
(mapv #(update % :role keyword))))
(sv/defmethod ::get-team-invitations
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(get-team-invitations conn team-id)))
;; --- Mutation: Create Team
(declare create-team)
(declare create-project)
(declare create-project-role)
(declare ^:private create-team*)
(declare ^:private create-team-role)
(declare ^:private create-team-default-project)
(s/def ::create-team
(s/keys :req [::rpc/profile-id]
:req-un [::name]
:opt-un [::id]))
(sv/defmethod ::create-team
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(create-team conn (assoc params :profile-id profile-id))))
(defn create-team
"This is a complete team creation process, it creates the team
object and all related objects (default role and default project)."
[conn params]
(let [team (create-team* conn params)
params (assoc params
:team-id (:id team)
:role :owner)
project (create-team-default-project conn params)]
(create-team-role conn params)
(assoc team :default-project-id (:id project))))
(defn- create-team*
[conn {:keys [id name is-default] :as params}]
(let [id (or id (uuid/next))
is-default (if (boolean? is-default) is-default false)]
(db/insert! conn :team
{:id id
:name name
:is-default is-default})))
(defn- create-team-role
[conn {:keys [profile-id team-id role] :as params}]
(let [params {:team-id team-id
:profile-id profile-id}]
(->> (perms/assign-role-flags params role)
(db/insert! conn :team-profile-rel))))
(defn- create-team-default-project
[conn {:keys [profile-id team-id] :as params}]
(let [project {:id (uuid/next)
:team-id team-id
:name "Drafts"
:is-default true}
project (create-project conn project)]
(create-project-role conn profile-id (:id project) :owner)
project))
;; NOTE: we have project creation here because there are cyclic
;; dependency between teams and projects namespaces, and the project
;; creation happens in both sides, on team creation and on simple
;; project creation, so it make sense to have this functions in this
;; namespace too.
(defn create-project
[conn {:keys [id team-id name is-default] :as params}]
(let [id (or id (uuid/next))
is-default (if (boolean? is-default) is-default false)]
(db/insert! conn :project
{:id id
:name name
:team-id team-id
:is-default is-default})))
(defn create-project-role
[conn profile-id project-id role]
(let [params {:project-id project-id
:profile-id profile-id}]
(->> (perms/assign-role-flags params role)
(db/insert! conn :project-profile-rel))))
;; --- Mutation: Update Team
(s/def ::update-team
(s/keys :req [::rpc/profile-id]
:req-un [::name ::id]))
(sv/defmethod ::update-team
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
(db/with-atomic [conn pool]
(check-edition-permissions! conn profile-id id)
(db/update! conn :team
{:name name}
{:id id})
nil))
;; --- Mutation: Leave Team
(declare role->params)
(s/def ::reassign-to ::us/uuid)
(s/def ::leave-team
(s/keys :req [::rpc/profile-id]
:req-un [::id]
:opt-un [::reassign-to]))
(defn leave-team
[conn {:keys [::rpc/profile-id id reassign-to]}]
(let [perms (get-permissions conn profile-id id)
members (retrieve-team-members conn id)]
(cond
;; we can only proceed if there are more members in the team
;; besides the current profile
(<= (count members) 1)
(ex/raise :type :validation
:code :no-enough-members-for-leave
:context {:members (count members)})
;; if the `reassign-to` is filled and has a different value
;; than the current profile-id, we proceed to reassing the
;; owner role to profile identified by the `reassign-to`.
(and reassign-to (not= reassign-to profile-id))
(let [member (d/seek #(= reassign-to (:id %)) members)]
(when-not member
(ex/raise :type :not-found :code :member-does-not-exist))
;; unasign owner role to current profile
(db/update! conn :team-profile-rel
{:is-owner false}
{:team-id id
:profile-id profile-id})
;; assign owner role to new profile
(db/update! conn :team-profile-rel
(role->params :owner)
{:team-id id :profile-id reassign-to}))
;; and finally, if all other conditions does not match and the
;; current profile is owner, we dont allow it because there
;; must always be an owner.
(:is-owner perms)
(ex/raise :type :validation
:code :owner-cant-leave-team
:hint "releasing owner before leave"))
(db/delete! conn :team-profile-rel
{:profile-id profile-id
:team-id id})
nil))
(sv/defmethod ::leave-team
{::doc/added "1.17"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(leave-team conn params)))
;; --- Mutation: Delete Team
(s/def ::delete-team
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
;; TODO: right now just don't allow delete default team, in future it
;; should raise a specific exception for signal that this action is
;; not allowed.
(sv/defmethod ::delete-team
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id id)]
(when-not (:is-owner perms)
(ex/raise :type :validation
:code :only-owner-can-delete-team))
(db/update! conn :team
{:deleted-at (dt/now)}
{:id id :is-default false})
nil)))
;; --- Mutation: Team Update Role
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/uxboxproject/issue/1083
;; (s/def ::role #{:owner :admin :editor :viewer})
(s/def ::role #{:owner :admin :editor})
(defn role->params
[role]
(case role
:admin {:is-owner false :is-admin true :can-edit true}
:editor {:is-owner false :is-admin false :can-edit true}
:owner {:is-owner true :is-admin true :can-edit true}
:viewer {:is-owner false :is-admin false :can-edit false}))
(defn update-team-member-role
[conn {:keys [profile-id team-id member-id role] :as params}]
;; We retrieve all team members instead of query the
;; database for a single member. This is just for
;; convenience, if this becomes a bottleneck or problematic,
;; we will change it to more efficient fetch mechanisms.
(let [perms (get-permissions conn profile-id team-id)
members (retrieve-team-members conn team-id)
member (d/seek #(= member-id (:id %)) members)
is-owner? (:is-owner perms)
is-admin? (:is-admin perms)]
;; If no member is found, just 404
(when-not member
(ex/raise :type :not-found
:code :member-does-not-exist))
;; First check if we have permissions to change roles
(when-not (or is-owner? is-admin?)
(ex/raise :type :validation
:code :insufficient-permissions))
;; Don't allow change role of owner member
(when (:is-owner member)
(ex/raise :type :validation
:code :cant-change-role-to-owner))
;; Don't allow promote to owner to admin users.
(when (and (not is-owner?) (= role :owner))
(ex/raise :type :validation
:code :cant-promote-to-owner))
(let [params (role->params role)]
;; Only allow single owner on team
(when (= role :owner)
(db/update! conn :team-profile-rel
{:is-owner false}
{:team-id team-id
:profile-id profile-id}))
(db/update! conn :team-profile-rel
params
{:team-id team-id
:profile-id member-id})
nil)))
(s/def ::update-team-member-role
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::member-id ::role]))
(sv/defmethod ::update-team-member-role
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
(update-team-member-role conn (assoc params :profile-id (::rpc/profile-id params)))))
;; --- Mutation: Delete Team Member
(s/def ::delete-team-member
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::member-id]))
(sv/defmethod ::delete-team-member
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)]
(when-not (or (:is-owner perms)
(:is-admin perms))
(ex/raise :type :validation
:code :insufficient-permissions))
(when (= member-id profile-id)
(ex/raise :type :validation
:code :cant-remove-yourself))
(db/delete! conn :team-profile-rel {:profile-id member-id
:team-id team-id})
nil)))
;; --- Mutation: Update Team Photo
(declare ^:private upload-photo)
(declare ^:private update-team-photo)
(s/def ::file ::media/upload)
(s/def ::update-team-photo
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::file]))
(sv/defmethod ::update-team-photo
{::doc/added "1.17"}
[cfg {:keys [::rpc/profile-id file] :as params}]
;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
(let [cfg (update cfg :storage media/configure-assets-storage)]
(update-team-photo cfg (assoc params :profile-id profile-id))))
(defn update-team-photo
[{:keys [pool storage executor] :as cfg} {:keys [profile-id team-id] :as params}]
(p/let [team (px/with-dispatch executor
(retrieve-team pool profile-id team-id))
photo (upload-photo cfg params)]
;; Mark object as touched for make it ellegible for tentative
;; garbage collection.
(when-let [id (:photo-id team)]
(sto/touch-object! storage id))
;; Save new photo
(db/update! pool :team
{:photo-id (:id photo)}
{:id team-id})
(assoc team :photo-id (:id photo))))
(defn upload-photo
[{:keys [storage executor climit] :as cfg} {:keys [file]}]
(letfn [(get-info [content]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :info :input content})))
(generate-thumbnail [info]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input info})))
;; Function responsible of calculating cryptographyc hash of
;; the provided data.
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))]
(p/let [info (get-info file)
thumb (generate-thumbnail info)
hash (calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
(sto/put-object! storage {::sto/content content
::sto/deduplicate? true
:bucket "profile"
:content-type (:mtype thumb)}))))
;; --- Mutation: Create Team Invitation
(def sql:upsert-team-invitation
"insert into team_invitation(team_id, email_to, role, valid_until)
values (?, ?, ?, ?)
on conflict(team_id, email_to) do
update set role = ?, updated_at = now();")
(defn- create-invitation-token
[cfg {:keys [profile-id valid-until team-id member-id member-email role]}]
(tokens/generate (::main/props cfg)
{:iss :team-invitation
:exp valid-until
:profile-id profile-id
:role role
:team-id team-id
:member-email member-email
:member-id member-id}))
(defn- create-profile-identity-token
[cfg profile]
(tokens/generate (::main/props cfg)
{:iss :profile-identity
:profile-id (:id profile)
:exp (dt/in-future {:days 30})}))
(defn- create-invitation
[{:keys [::conn] :as cfg} {:keys [team profile role email] :as params}]
(let [member (profile/retrieve-profile-data-by-email conn email)
expire (dt/in-future "168h") ;; 7 days
itoken (create-invitation-token cfg {:profile-id (:id profile)
:valid-until expire
:team-id (:id team)
:member-email (or (:email member) email)
:member-id (:id member)
:role role})
ptoken (create-profile-identity-token cfg profile)]
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
:code :member-is-muted
:email email
:hint "the profile has reported repeatedly as spam or has bounces"))
;; Secondly check if the invited member email is part of the global spam/bounce report.
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
:code :email-has-permanent-bounces
:email email
:hint "the email you invite has been repeatedly reported as spam or bounce"))
(when (contains? cf/flags :log-invitation-tokens)
(l/trace :hint "invitation token" :token itoken))
;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the
;; team as-is, without email roundtrip.
;; TODO: if member does not exists and email verification is
;; disabled, we should proceed to create the profile (?)
(if (and (not (contains? cf/flags :email-verification))
(some? member))
(let [params (merge {:team-id (:id team)
:profile-id (:id member)}
(role->params role))]
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
;; If profile is not yet verified, mark it as verified because
;; accepting an invitation link serves as verification.
(when-not (:is-active member)
(db/update! conn :profile
{:is-active true}
{:id (:id member)})))
(do
(db/exec-one! conn [sql:upsert-team-invitation
(:id team) (str/lower email) (name role) expire (name role)])
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
:public-uri (cf/get :public-uri)
:to email
:invited-by (:fullname profile)
:team (:name team)
:token itoken
:extra-data ptoken})))
itoken))
(s/def ::email ::us/email)
(s/def ::emails ::us/set-of-valid-emails)
(s/def ::create-team-invitations
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::role]
:opt-un [::email ::emails]))
(sv/defmethod ::create-team-invitations
"A rpc call that allow to send a single or multiple invitations to
join the team."
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)
emails (cond-> (or emails #{}) (string? email) (conj email))]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
;; First check if the current profile is allowed to send emails.
(when-not (eml/allow-send-emails? conn profile)
(ex/raise :type :validation
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(let [cfg (assoc cfg ::conn conn)
invitations (->> emails
(map (fn [email]
{:email (str/lower email)
:team team
:profile profile
:role role}))
(map (partial create-invitation cfg)))]
(with-meta (vec invitations)
{::audit/props {:invitations (count invitations)}})))))
;; --- Mutation: Create Team & Invite Members
(s/def ::emails ::us/set-of-valid-emails)
(s/def ::create-team-with-invitations
(s/merge ::create-team
(s/keys :req-un [::emails ::role])))
(sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [team (create-team conn params)
profile (db/get-by-id conn :profile profile-id)
cfg (assoc cfg ::conn conn)]
;; Create invitations for all provided emails.
(->> emails
(map (fn [email]
{:team team
:profile profile
:email (str/lower email)
:role role}))
(run! (partial create-invitation cfg)))
(-> team
(vary-meta assoc ::audit/props {:invitations (count emails)})
(rph/with-defer
#(when-let [collector (::audit/collector cfg)]
(audit/submit! collector
{:type "command"
:name "create-team-invitations"
:profile-id profile-id
:props {:emails emails
:role role
:profile-id profile-id
:invitations (count emails)}})))))))
;; --- Query: get-team-invitation-token
(s/def ::get-team-invitation-token
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::email]))
(sv/defmethod ::get-team-invitation-token
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
(check-read-permissions! pool profile-id team-id)
(let [invit (-> (db/get pool :team-invitation
{:team-id team-id
:email-to (str/lower email)})
(update :role keyword))
member (profile/retrieve-profile-data-by-email pool (:email invit))
token (create-invitation-token cfg {:team-id (:team-id invit)
:profile-id profile-id
:valid-until (:valid-until invit)
:role (:role invit)
:member-id (:id member)
:member-email (or (:email member) (:email-to invit))})]
{:token token}))
;; --- Mutation: Update invitation role
(s/def ::update-team-invitation-role
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::email ::role]))
(sv/defmethod ::update-team-invitation-role
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(db/update! conn :team-invitation
{:role (name role) :updated-at (dt/now)}
{:team-id team-id :email-to (str/lower email)})
nil)))
;; --- Mutation: Delete invitation
(s/def ::delete-team-invitation
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::email]))
(sv/defmethod ::delete-team-invitation
{::doc/added "1.17"}
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(db/delete! conn :team-invitation
{:team-id team-id :email-to (str/lower email)})
nil)))

View File

@@ -11,10 +11,9 @@
[app.db :as db]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.tokens :as tokens]
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
@@ -28,10 +27,10 @@
(s/def ::verify-token
(s/keys :req-un [::token]
:opt [::rpc/profile-id]))
:opt-un [::profile-id]))
(sv/defmethod ::verify-token
{::rpc/auth false
{:auth false
::doc/added "1.15"}
[{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool]
@@ -127,11 +126,10 @@
:opt-un [::spec.team-invitation/member-id]))
(defmethod process-token :team-invitation
[{:keys [conn session] :as cfg}
{:keys [::rpc/profile-id token]}
[{:keys [conn session] :as cfg} {:keys [profile-id token]}
{:keys [member-id team-id member-email] :as claims}]
(us/verify! ::team-invitation-claims claims)
(us/assert ::team-invitation-claims claims)
(let [invitation (db/get* conn :team-invitation
{:team-id team-id :email-to member-email})

View File

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

View File

@@ -12,9 +12,8 @@
[app.db :as db]
[app.http.client :as http]
[app.loggers.webhooks :as webhooks]
[app.rpc :as-alias rpc]
[app.rpc.commands.teams :refer [check-edition-permissions! check-read-permissions!]]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.teams :refer [check-edition-permissions! check-read-permissions!]]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
@@ -24,6 +23,7 @@
;; --- Mutation: Create Webhook
(s/def ::profile-id ::us/uuid)
(s/def ::team-id ::us/uuid)
(s/def ::uri ::us/not-empty-string)
(s/def ::is-active ::us/boolean)
@@ -33,8 +33,7 @@
"application/transit+json"})
(s/def ::create-webhook
(s/keys :req [::rpc/profile-id]
:req-un [::team-id ::uri ::mtype]
(s/keys :req-un [::profile-id ::team-id ::uri ::mtype]
:opt-un [::is-active]))
;; NOTE: for now the quote is hardcoded but this need to be solved in
@@ -75,8 +74,7 @@
(when (>= total max-hooks-for-team)
(ex/raise :type :restriction
:code :webhooks-quote-reached
:hint (str/ffmt "can't create more than % webhooks per team"
max-hooks-for-team)))))
:hint (str/ffmt "can't create more than % webhooks per team" max-hooks-for-team)))))
(defn- insert-webhook!
[{:keys [::db/pool]} {:keys [team-id uri mtype is-active] :as params}]
@@ -99,10 +97,10 @@
(sv/defmethod ::create-webhook
{::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id team-id] :as params}]
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [profile-id team-id] :as params}]
(check-edition-permissions! pool profile-id team-id)
(validate-quotes! cfg params)
(->> (validate-webhook! cfg nil params)
(->> (validate-quotes! cfg params)
(p/fmap executor (fn [_] (validate-webhook! cfg nil params)))
(p/fmap executor (fn [_] (insert-webhook! cfg params)))))
(s/def ::update-webhook
@@ -110,19 +108,18 @@
(sv/defmethod ::update-webhook
{::doc/added "1.17"}
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [::rpc/profile-id id] :as params}]
[{:keys [::db/pool ::wrk/executor] :as cfg} {:keys [id profile-id] :as params}]
(let [whook (db/get pool :webhook {:id id})]
(check-edition-permissions! pool profile-id (:team-id whook))
(->> (validate-webhook! cfg whook params)
(p/fmap executor (fn [_] (update-webhook! cfg whook params))))))
(s/def ::delete-webhook
(s/keys :req [::rpc/profile-id]
:req-un [::id]))
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::delete-webhook
{::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
[{:keys [::db/pool] :as cfg} {:keys [profile-id id]}]
(db/with-atomic [conn pool]
(let [whook (db/get conn :webhook {:id id})]
(check-edition-permissions! conn profile-id (:team-id whook))
@@ -133,15 +130,14 @@
(s/def ::team-id ::us/uuid)
(s/def ::get-webhooks
(s/keys :req [::rpc/profile-id]
:req-un [::team-id]))
(s/keys :req-un [::profile-id ::team-id]))
(def sql:get-webhooks
"select id, uri, mtype, is_active, error_code, error_count
from webhook where team_id = ? order by uri")
(sv/defmethod ::get-webhooks
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id team-id]}]
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(check-read-permissions! conn profile-id team-id)
(db/exec! conn [sql:get-webhooks team-id])))

View File

@@ -9,7 +9,6 @@
(:require
[app.common.data :as d]
[app.config :as cf]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc :as-alias rpc]
[app.util.services :as sv]
[app.util.template :as tmpl]
@@ -36,7 +35,6 @@
:name (d/name name)
:module (-> (:ns mdata) (str/split ".") last)
:auth (:auth mdata true)
:webhook (::webhooks/event? mdata false)
:docs (::sv/docstring mdata)
:deprecated (::deprecated mdata)
:added (::added mdata)
@@ -53,7 +51,6 @@
(->> (:queries methods)
(map (partial gen-doc :query))
(sort-by (juxt :module :name)))
:mutation-methods
(->> (:mutations methods)
(map (partial gen-doc :query))

View File

@@ -0,0 +1,123 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.mutations.comments
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.rpc.commands.comments :as cmd.comments]
[app.rpc.commands.files :as cmd.files]
[app.rpc.doc :as-alias doc]
[app.rpc.retry :as retry]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Mutation: Create Comment Thread
(s/def ::create-comment-thread ::cmd.comments/create-comment-thread)
(sv/defmethod ::create-comment-thread
{::retry/max-retries 3
::retry/matches retry/conflict-db-insert?
::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
(db/with-atomic [conn pool]
(cmd.files/check-comment-permissions! conn profile-id file-id share-id)
(cmd.comments/create-comment-thread conn params)))
;; --- Mutation: Update Comment Thread Status
(s/def ::id ::us/uuid)
(s/def ::share-id (s/nilable ::us/uuid))
(s/def ::update-comment-thread-status ::cmd.comments/update-comment-thread-status)
(sv/defmethod ::update-comment-thread-status
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id share-id] :as params}]
(db/with-atomic [conn pool]
(let [cthr (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not cthr (ex/raise :type :not-found))
(cmd.files/check-comment-permissions! conn profile-id (:file-id cthr) share-id)
(cmd.comments/upsert-comment-thread-status! conn profile-id (:id cthr)))))
;; --- Mutation: Update Comment Thread
(s/def ::update-comment-thread ::cmd.comments/update-comment-thread)
(sv/defmethod ::update-comment-thread
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id is-resolved share-id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not thread
(ex/raise :type :not-found))
(cmd.files/check-comment-permissions! conn profile-id (:file-id thread) share-id)
(db/update! conn :comment-thread
{:is-resolved is-resolved}
{:id id})
nil)))
;; --- Mutation: Add Comment
(s/def ::add-comment ::cmd.comments/create-comment)
(sv/defmethod ::add-comment
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(cmd.comments/create-comment conn params)))
;; --- Mutation: Update Comment
(s/def ::update-comment ::cmd.comments/update-comment)
(sv/defmethod ::update-comment
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(cmd.comments/update-comment conn params)))
;; --- Mutation: Delete Comment Thread
(s/def ::delete-comment-thread ::cmd.comments/delete-comment-thread)
(sv/defmethod ::delete-comment-thread
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
(when-not (= (:owner-id thread) profile-id)
(ex/raise :type :validation :code :not-allowed))
(db/delete! conn :comment-thread {:id id})
nil)))
;; --- Mutation: Delete comment
(s/def ::delete-comment ::cmd.comments/delete-comment)
(sv/defmethod ::delete-comment
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
(db/with-atomic [conn pool]
(let [comment (db/get-by-id conn :comment id {:for-update true})]
(when-not (= (:owner-id comment) profile-id)
(ex/raise :type :validation :code :not-allowed))
(db/delete! conn :comment {:id id}))))

View File

@@ -81,8 +81,7 @@
(db/with-atomic [conn pool]
(cmd.files/check-edition-permissions! conn profile-id id)
(cmd.files/absorb-library conn params)
(cmd.files/mark-file-deleted conn params)
nil))
(cmd.files/mark-file-deleted conn params)))
;; --- Mutation: Link file to library

View File

@@ -15,9 +15,9 @@
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.teams :as teams]
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -110,12 +110,12 @@
]
(->> (generate-fonts data)
(p/fmap validate-data)
(p/map validate-data)
(p/mcat executor persist-fonts)
(p/fmap executor insert-into-db)
(p/fmap (fn [result]
(let [params (update params :data (comp vec keys))]
(rph/with-meta result {::audit/replace-props params})))))))
(p/map executor insert-into-db)
(p/map (fn [result]
(let [params (update params :data (comp vec keys))]
(rph/with-meta result {::audit/replace-props params})))))))
;; --- UPDATE FONT FAMILY
@@ -128,15 +128,10 @@
[{:keys [pool] :as cfg} {:keys [team-id profile-id id name] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(rph/with-meta
(db/update! conn :team-font-variant
{:font-family name}
{:font-id id
:team-id team-id})
{::audit/replace-props {:id id
:name name
:team-id team-id
:profile-id profile-id}})))
(db/update! conn :team-font-variant
{:font-family name}
{:font-id id
:team-id team-id})))
;; --- DELETE FONT
@@ -149,14 +144,10 @@
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [font (db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:font-id id :team-id team-id})]
(rph/with-meta (rph/wrap)
{::audit/props {:id id
:team-id team-id
:name (:font-family font)
:profile-id profile-id}}))))
(db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:font-id id :team-id team-id})
nil))
;; --- DELETE FONT VARIANT
@@ -169,9 +160,8 @@
[{:keys [pool] :as cfg} {:keys [id team-id profile-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [variant (db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:id id :team-id team-id})]
(rph/with-meta (rph/wrap)
{::audit/props {:font-family (:font-family variant)
:font-id (:font-id variant)}}))))
(db/update! conn :team-font-variant
{:deleted-at (dt/now)}
{:id id :team-id team-id})
nil))

View File

@@ -0,0 +1,58 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.mutations.management
"Move & Duplicate RPC methods for files and projects."
(:require
[app.db :as db]
[app.rpc.commands.management :as cmd.mgm]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- MUTATION: Duplicate File
(s/def ::duplicate-file ::cmd.mgm/duplicate-file)
(sv/defmethod ::duplicate-file
{::doc/added "1.2"
::doc/deprecated "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(cmd.mgm/duplicate-file conn params)))
;; --- MUTATION: Duplicate Project
(s/def ::duplicate-project ::cmd.mgm/duplicate-project)
(sv/defmethod ::duplicate-project
{::doc/added "1.2"
::doc/deprecated "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(cmd.mgm/duplicate-project conn params)))
;; --- MUTATION: Move file
(s/def ::move-files ::cmd.mgm/move-files)
(sv/defmethod ::move-files
{::doc/added "1.2"
::doc/deprecated "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(cmd.mgm/move-files conn params)))
;; --- MUTATION: Move project
(s/def ::move-project ::cmd.mgm/move-project)
(sv/defmethod ::move-project
{::doc/added "1.2"
::doc/deprecated "1.16"}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(cmd.mgm/move-project conn params)))

View File

@@ -16,7 +16,7 @@
[app.http.client :as http]
[app.media :as media]
[app.rpc.climit :as climit]
[app.rpc.commands.teams :as teams]
[app.rpc.queries.teams :as teams]
[app.storage :as sto]
[app.storage.tmp :as tmp]
[app.util.services :as sv]

View File

@@ -6,7 +6,6 @@
(ns app.rpc.mutations.profile
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
@@ -19,9 +18,9 @@
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.storage :as sto]
[app.tokens :as tokens]
@@ -112,7 +111,7 @@
(defn- validate-password!
[conn {:keys [profile-id old-password] :as params}]
(let [profile (db/get-by-id conn :profile profile-id)]
(when-not (:valid (auth/verify-password old-password (:password profile)))
(when-not (:valid (cmd.auth/verify-password old-password (:password profile)))
(ex/raise :type :validation
:code :old-password-not-match))
profile))
@@ -120,7 +119,7 @@
(defn update-profile-password!
[conn {:keys [id password] :as profile}]
(db/update! conn :profile
{:password (auth/derive-password password)}
{:password (cmd.auth/derive-password password)}
{:id id}))
;; --- MUTATION: Update Photo

View File

@@ -7,13 +7,15 @@
(ns app.rpc.mutations.projects
(:require
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.permissions :as perms]
[app.rpc.queries.projects :as proj]
[app.rpc.queries.teams :as teams]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]))
@@ -26,6 +28,10 @@
;; --- Mutation: Create Project
(declare create-project)
(declare create-project-role)
(declare create-team-project-profile)
(s/def ::team-id ::us/uuid)
(s/def ::create-project
(s/keys :req-un [::profile-id ::team-id ::name]
@@ -37,17 +43,41 @@
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id)
(let [project (teams/create-project conn params)]
(teams/create-project-role conn profile-id (:id project) :owner)
(db/insert! conn :team-project-profile-rel
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned true})
(let [project (create-project conn params)
params (assoc params
:project-id (:id project)
:role :owner)]
(create-project-role conn params)
(create-team-project-profile conn params)
(assoc project :is-pinned true))))
(defn create-project
[conn {:keys [id team-id name is-default] :as params}]
(let [id (or id (uuid/next))
is-default (if (boolean? is-default) is-default false)]
(db/insert! conn :project
{:id id
:name name
:team-id team-id
:is-default is-default})))
(defn create-project-role
[conn {:keys [project-id profile-id role]}]
(let [params {:project-id project-id
:profile-id profile-id}]
(->> (perms/assign-role-flags params role)
(db/insert! conn :project-profile-rel))))
;; TODO: pending to be refactored
(defn create-team-project-profile
[conn {:keys [team-id project-id profile-id] :as params}]
(db/insert! conn :team-project-profile-rel
{:project-id project-id
:profile-id profile-id
:team-id team-id
:is-pinned true}))
;; --- Mutation: Toggle Project Pin
(def ^:private
@@ -64,16 +94,13 @@
(s/keys :req-un [::profile-id ::id ::team-id ::is-pinned]))
(sv/defmethod ::update-project-pin
{::doc/added "1.0"
::webhooks/batch-timeout (dt/duration "5s")
::webhooks/batch-key :id
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [id profile-id team-id is-pinned] :as params}]
(db/with-atomic [conn pool]
(proj/check-edition-permissions! conn profile-id id)
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
nil))
;; --- Mutation: Rename Project
(declare rename-project)
@@ -82,19 +109,13 @@
(s/keys :req-un [::profile-id ::name ::id]))
(sv/defmethod ::rename-project
{::doc/added "1.0"
::webhooks/event? true}
[{:keys [pool] :as cfg} {:keys [id profile-id name] :as params}]
(db/with-atomic [conn pool]
(proj/check-edition-permissions! conn profile-id id)
(let [project (db/get-by-id conn :project id)]
(db/update! conn :project
{:name name}
{:id id})
(rph/with-meta (rph/wrap)
{::audit/props {:team-id (:team-id project)
:prev-name (:name project)}}))))
(db/update! conn :project
{:name name}
{:id id})
nil))
;; --- Mutation: Delete Project
@@ -115,7 +136,4 @@
{:deleted-at (dt/now)}
{:id id :is-default false})]
(rph/with-meta (rph/wrap)
{::audit/props {:team-id (:team-id project)
:name (:name project)
:created-at (:created-at project)
:modified-at (:modified-at project)}}))))
{::audit/props {:team-id (:team-id project)}}))))

View File

@@ -6,19 +6,30 @@
(ns app.rpc.mutations.teams
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.loggers.audit :as audit]
[app.media :as media]
[app.rpc.commands.teams :as cmd.teams]
[app.rpc.doc :as-alias doc]
[app.rpc.climit :as climit]
[app.rpc.helpers :as rph]
[app.rpc.mutations.projects :as projects]
[app.rpc.permissions :as perms]
[app.rpc.queries.profile :as profile]
[app.rpc.queries.teams :as teams]
[app.storage :as sto]
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[promesa.core :as p]
[promesa.exec :as px]))
;; --- Helpers & Specs
@@ -28,54 +39,148 @@
;; --- Mutation: Create Team
(s/def ::create-team ::cmd.teams/create-team)
(declare create-team)
(declare create-team-entry)
(declare create-team-role)
(declare create-team-default-project)
(s/def ::create-team
(s/keys :req-un [::profile-id ::name]
:opt-un [::id]))
(sv/defmethod ::create-team
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} params]
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(cmd.teams/create-team conn params)))
(create-team conn params)))
(defn create-team
"This is a complete team creation process, it creates the team
object and all related objects (default role and default project)."
[conn params]
(let [team (create-team-entry conn params)
params (assoc params
:team-id (:id team)
:role :owner)
project (create-team-default-project conn params)]
(create-team-role conn params)
(assoc team :default-project-id (:id project))))
(defn- create-team-entry
[conn {:keys [id name is-default] :as params}]
(let [id (or id (uuid/next))
is-default (if (boolean? is-default) is-default false)]
(db/insert! conn :team
{:id id
:name name
:is-default is-default})))
(defn- create-team-role
[conn {:keys [team-id profile-id role] :as params}]
(let [params {:team-id team-id
:profile-id profile-id}]
(->> (perms/assign-role-flags params role)
(db/insert! conn :team-profile-rel))))
(defn- create-team-default-project
[conn {:keys [team-id profile-id] :as params}]
(let [project {:id (uuid/next)
:team-id team-id
:name "Drafts"
:is-default true}
project (projects/create-project conn project)]
(projects/create-project-role conn {:project-id (:id project)
:profile-id profile-id
:role :owner})
project))
;; --- Mutation: Update Team
(s/def ::update-team ::cmd.teams/update-team)
(s/def ::update-team
(s/keys :req-un [::profile-id ::name ::id]))
(sv/defmethod ::update-team
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [id name profile-id] :as params}]
[{:keys [pool] :as cfg} {:keys [id name profile-id] :as params}]
(db/with-atomic [conn pool]
(cmd.teams/check-edition-permissions! conn profile-id id)
(teams/check-edition-permissions! conn profile-id id)
(db/update! conn :team
{:name name}
{:id id})
nil))
;; --- Mutation: Leave Team
(s/def ::leave-team ::cmd.teams/leave-team)
(declare role->params)
(s/def ::reassign-to ::us/uuid)
(s/def ::leave-team
(s/keys :req-un [::profile-id ::id]
:opt-un [::reassign-to]))
(sv/defmethod ::leave-team
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [id profile-id reassign-to]}]
(db/with-atomic [conn pool]
(cmd.teams/leave-team conn params)))
(let [perms (teams/get-permissions conn profile-id id)
members (teams/retrieve-team-members conn id)]
(cond
;; we can only proceed if there are more members in the team
;; besides the current profile
(<= (count members) 1)
(ex/raise :type :validation
:code :no-enough-members-for-leave
:context {:members (count members)})
;; if the `reassign-to` is filled and has a different value
;; than the current profile-id, we proceed to reassing the
;; owner role to profile identified by the `reassign-to`.
(and reassign-to (not= reassign-to profile-id))
(let [member (d/seek #(= reassign-to (:id %)) members)]
(when-not member
(ex/raise :type :not-found :code :member-does-not-exist))
;; unasign owner role to current profile
(db/update! conn :team-profile-rel
{:is-owner false}
{:team-id id
:profile-id profile-id})
;; assign owner role to new profile
(db/update! conn :team-profile-rel
(role->params :owner)
{:team-id id :profile-id reassign-to}))
;; and finally, if all other conditions does not match and the
;; current profile is owner, we dont allow it because there
;; must always be an owner.
(:is-owner perms)
(ex/raise :type :validation
:code :owner-cant-leave-team
:hint "releasing owner before leave"))
(db/delete! conn :team-profile-rel
{:profile-id profile-id
:team-id id})
nil)))
;; --- Mutation: Delete Team
(s/def ::delete-team ::cmd.teams/delete-team)
(s/def ::delete-team
(s/keys :req-un [::profile-id ::id]))
;; TODO: right now just don't allow delete default team, in future it
;; should raise a specific exception for signal that this action is
;; not allowed.
(sv/defmethod ::delete-team
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [id profile-id] :as params}]
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
(db/with-atomic [conn pool]
(let [perms (cmd.teams/get-permissions conn profile-id id)]
(let [perms (teams/get-permissions conn profile-id id)]
(when-not (:is-owner perms)
(ex/raise :type :validation
:code :only-owner-can-delete-team))
(db/update! conn :team
{:deleted-at (dt/now)}
{:id id :is-default false})
@@ -84,29 +189,89 @@
;; --- Mutation: Team Update Role
(s/def ::update-team-member-role ::cmd.teams/update-team-member-role)
(declare retrieve-team-member)
(s/def ::team-id ::us/uuid)
(s/def ::member-id ::us/uuid)
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/uxboxproject/issue/1083
;; (s/def ::role #{:owner :admin :editor :viewer})
(s/def ::role #{:owner :admin :editor})
(s/def ::update-team-member-role
(s/keys :req-un [::profile-id ::team-id ::member-id ::role]))
(sv/defmethod ::update-team-member-role
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} params]
[{:keys [pool] :as cfg} {:keys [team-id profile-id member-id role] :as params}]
(db/with-atomic [conn pool]
(cmd.teams/update-team-member-role conn params)))
(let [perms (teams/get-permissions conn profile-id team-id)
;; We retrieve all team members instead of query the
;; database for a single member. This is just for
;; convenience, if this becomes a bottleneck or problematic,
;; we will change it to more efficient fetch mechanisms.
members (teams/retrieve-team-members conn team-id)
member (d/seek #(= member-id (:id %)) members)
is-owner? (:is-owner perms)
is-admin? (:is-admin perms)]
;; If no member is found, just 404
(when-not member
(ex/raise :type :not-found
:code :member-does-not-exist))
;; First check if we have permissions to change roles
(when-not (or is-owner? is-admin?)
(ex/raise :type :validation
:code :insufficient-permissions))
;; Don't allow change role of owner member
(when (:is-owner member)
(ex/raise :type :validation
:code :cant-change-role-to-owner))
;; Don't allow promote to owner to admin users.
(when (and (not is-owner?) (= role :owner))
(ex/raise :type :validation
:code :cant-promote-to-owner))
(let [params (role->params role)]
;; Only allow single owner on team
(when (= role :owner)
(db/update! conn :team-profile-rel
{:is-owner false}
{:team-id team-id
:profile-id profile-id}))
(db/update! conn :team-profile-rel
params
{:team-id team-id
:profile-id member-id})
nil))))
(defn role->params
[role]
(case role
:admin {:is-owner false :is-admin true :can-edit true}
:editor {:is-owner false :is-admin false :can-edit true}
:owner {:is-owner true :is-admin true :can-edit true}
:viewer {:is-owner false :is-admin false :can-edit false}))
;; --- Mutation: Delete Team Member
(s/def ::delete-team-member ::cmd.teams/delete-team-member)
(s/def ::delete-team-member
(s/keys :req-un [::profile-id ::team-id ::member-id]))
(sv/defmethod ::delete-team-member
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [team-id profile-id member-id] :as params}]
[{:keys [pool] :as cfg} {:keys [team-id profile-id member-id] :as params}]
(db/with-atomic [conn pool]
(let [perms (cmd.teams/get-permissions conn profile-id team-id)]
(let [perms (teams/get-permissions conn profile-id team-id)]
(when-not (or (:is-owner perms)
(:is-admin perms))
(ex/raise :type :validation
:code :insufficient-permissions))
(when (= member-id profile-id)
(ex/raise :type :validation
:code :cant-remove-yourself))
@@ -118,27 +283,85 @@
;; --- Mutation: Update Team Photo
(s/def ::update-team-photo ::cmd.teams/update-team-photo)
(declare ^:private upload-photo)
(declare ^:private update-team-photo)
(s/def ::file ::media/upload)
(s/def ::update-team-photo
(s/keys :req-un [::profile-id ::team-id ::file]))
(sv/defmethod ::update-team-photo
{::doc/added "1.0"
::doc/deprecated "1.17"}
[cfg {:keys [file] :as params}]
;; Validate incoming mime type
(media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"})
(let [cfg (update cfg :storage media/configure-assets-storage)]
(cmd.teams/update-team-photo cfg params)))
(update-team-photo cfg params)))
(defn update-team-photo
[{:keys [pool storage executor] :as cfg} {:keys [profile-id team-id] :as params}]
(p/let [team (px/with-dispatch executor
(teams/retrieve-team pool profile-id team-id))
photo (upload-photo cfg params)]
;; Mark object as touched for make it ellegible for tentative
;; garbage collection.
(when-let [id (:photo-id team)]
(sto/touch-object! storage id))
;; Save new photo
(db/update! pool :team
{:photo-id (:id photo)}
{:id team-id})
(assoc team :photo-id (:id photo))))
(defn upload-photo
[{:keys [storage executor climit] :as cfg} {:keys [file]}]
(letfn [(get-info [content]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :info :input content})))
(generate-thumbnail [info]
(climit/with-dispatch (:process-image climit)
(media/run {:cmd :profile-thumbnail
:format :jpeg
:quality 85
:width 256
:height 256
:input info})))
;; Function responsible of calculating cryptographyc hash of
;; the provided data.
(calculate-hash [data]
(px/with-dispatch executor
(sto/calculate-hash data)))]
(p/let [info (get-info file)
thumb (generate-thumbnail info)
hash (calculate-hash (:data thumb))
content (-> (sto/content (:data thumb) (:size thumb))
(sto/wrap-with-hash hash))]
(sto/put-object! storage {::sto/content content
::sto/deduplicate? true
:bucket "profile"
:content-type (:mtype thumb)}))))
;; --- Mutation: Invite Member
(s/def ::invite-team-member ::cmd.teams/create-team-invitations)
(declare create-team-invitation)
(s/def ::email ::us/email)
(s/def ::emails ::us/set-of-valid-emails)
(s/def ::invite-team-member
(s/keys :req-un [::profile-id ::team-id ::role]
:opt-un [::email ::emails]))
(sv/defmethod ::invite-team-member
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}]
"A rpc call that allow to send a single or multiple invitations to
join the team."
[{:keys [pool] :as cfg} {:keys [profile-id team-id email emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (cmd.teams/get-permissions conn profile-id team-id)
(let [perms (teams/get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)
emails (cond-> (or emails #{}) (string? email) (conj email))]
@@ -153,38 +376,115 @@
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(let [cfg (assoc cfg ::cmd.teams/conn conn)
invitations (->> emails
(let [invitations (->> emails
(map (fn [email]
{:email (str/lower email)
:team team
:profile profile
:role role}))
(map (partial #'cmd.teams/create-invitation cfg)))]
(assoc cfg
:email email
:conn conn
:team team
:profile profile
:role role)))
(map create-team-invitation))]
(with-meta (vec invitations)
{::audit/props {:invitations (count invitations)}})))))
(def sql:upsert-team-invitation
"insert into team_invitation(team_id, email_to, role, valid_until)
values (?, ?, ?, ?)
on conflict(team_id, email_to) do
update set role = ?, valid_until = ?, updated_at = now();")
(defn- create-team-invitation
[{:keys [conn sprops team profile role email] :as cfg}]
(let [member (profile/retrieve-profile-data-by-email conn email)
token-exp (dt/in-future "168h") ;; 7 days
email (str/lower email)
itoken (tokens/generate sprops
{:iss :team-invitation
:exp token-exp
:profile-id (:id profile)
:role role
:team-id (:id team)
:member-email (:email member email)
:member-id (:id member)})
ptoken (tokens/generate sprops
{:iss :profile-identity
:profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
(when (and member (not (eml/allow-send-emails? conn member)))
(ex/raise :type :validation
:code :member-is-muted
:email email
:hint "the profile has reported repeatedly as spam or has bounces"))
;; Secondly check if the invited member email is part of the global spam/bounce report.
(when (eml/has-bounce-reports? conn email)
(ex/raise :type :validation
:code :email-has-permanent-bounces
:email email
:hint "the email you invite has been repeatedly reported as spam or bounce"))
(when (contains? cf/flags :log-invitation-tokens)
(l/trace :hint "invitation token" :token itoken))
;; When we have email verification disabled and invitation user is
;; already present in the database, we proceed to add it to the
;; team as-is, without email roundtrip.
;; TODO: if member does not exists and email verification is
;; disabled, we should proceed to create the profile (?)
(if (and (not (contains? cf/flags :email-verification))
(some? member))
(let [params (merge {:team-id (:id team)
:profile-id (:id member)}
(role->params role))]
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
;; If profile is not yet verified, mark it as verified because
;; accepting an invitation link serves as verification.
(when-not (:is-active member)
(db/update! conn :profile
{:is-active true}
{:id (:id member)})))
(do
(db/exec-one! conn [sql:upsert-team-invitation
(:id team) (str/lower email) (name role)
token-exp (name role) token-exp])
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-team
:public-uri (:public-uri cfg)
:to email
:invited-by (:fullname profile)
:team (:name team)
:token itoken
:extra-data ptoken})))
itoken))
;; --- Mutation: Create Team & Invite Members
(s/def ::create-team-and-invite-members ::cmd.teams/create-team-with-invitations)
(s/def ::emails ::us/set-of-valid-emails)
(s/def ::create-team-and-invite-members
(s/and ::create-team (s/keys :req-un [::emails ::role])))
(sv/defmethod ::create-team-and-invite-members
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id emails role] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [team (cmd.teams/create-team conn params)
profile (db/get-by-id conn :profile profile-id)
cfg (assoc cfg ::cmd.teams/conn conn)]
(let [team (create-team conn params)
profile (db/get-by-id conn :profile profile-id)]
;; Create invitations for all provided emails.
(->> emails
(map (fn [email]
{:team team
:profile profile
:email (str/lower email)
:role role}))
(run! (partial #'cmd.teams/create-invitation cfg)))
(doseq [email emails]
(create-team-invitation
(assoc cfg
:conn conn
:team team
:profile profile
:email email
:role role)))
(-> team
(vary-meta assoc ::audit/props {:invitations (count emails)})
@@ -205,11 +505,9 @@
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
(sv/defmethod ::update-team-invitation-role
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
(db/with-atomic [conn pool]
(let [perms (cmd.teams/get-permissions conn profile-id team-id)]
(let [perms (teams/get-permissions conn profile-id team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation
@@ -222,14 +520,13 @@
;; --- Mutation: Delete invitation
(s/def ::delete-team-invitation ::cmd.teams/delete-team-invitation)
(s/def ::delete-team-invitation
(s/keys :req-un [::profile-id ::team-id ::email]))
(sv/defmethod ::delete-team-invitation
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [::db/pool] :as cfg} {:keys [profile-id team-id email] :as params}]
[{:keys [pool] :as cfg} {:keys [profile-id team-id email] :as params}]
(db/with-atomic [conn pool]
(let [perms (cmd.teams/get-permissions conn profile-id team-id)]
(let [perms (teams/get-permissions conn profile-id team-id)]
(when-not (:is-admin perms)
(ex/raise :type :validation

View File

@@ -0,0 +1,28 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.mutations.verify-token
(:require
[app.db :as db]
[app.rpc.commands.verify-token :refer [process-token]]
[app.rpc.doc :as-alias doc]
[app.tokens :as tokens]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
(s/def ::verify-token
(s/keys :req-un [::token]
:opt-un [::profile-id]))
(sv/defmethod ::verify-token
{:auth false
::doc/added "1.1"
::doc/deprecated "1.15"}
[{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool]
(let [claims (tokens/verify sprops {:token token})
cfg (assoc cfg :conn conn)]
(process-token cfg params claims))))

View File

@@ -0,0 +1,82 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.rpc.queries.comments
(:require
[app.db :as db]
[app.rpc.commands.comments :as cmd.comments]
[app.rpc.commands.files :as cmd.files]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.teams :as teams]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
(defn decode-row
[{:keys [participants position] :as row}]
(cond-> row
(db/pgpoint? position) (assoc :position (db/decode-pgpoint position))
(db/pgobject? participants) (assoc :participants (db/decode-transit-pgobject participants))))
;; --- QUERY: Comment Threads
(s/def ::comment-threads ::cmd.comments/get-comment-threads)
(sv/defmethod ::comment-threads
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} params]
(with-open [conn (db/open pool)]
(cmd.comments/retrieve-comment-threads conn params)))
;; --- QUERY: Unread Comment Threads
(s/def ::unread-comment-threads ::cmd.comments/get-unread-comment-threads)
(sv/defmethod ::unread-comment-threads
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(cmd.comments/retrieve-unread-comment-threads conn params)))
;; --- QUERY: Single Comment Thread
(s/def ::comment-thread ::cmd.comments/get-comment-thread)
(sv/defmethod ::comment-thread
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
(with-open [conn (db/open pool)]
(cmd.files/check-comment-permissions! conn profile-id file-id share-id)
(cmd.comments/get-comment-thread conn params)))
;; --- QUERY: Comments
(s/def ::comments ::cmd.comments/get-comments)
(sv/defmethod ::comments
{::doc/added "1.0"
::doc/deprecated "1.15"}
[{:keys [pool] :as cfg} {:keys [profile-id thread-id share-id] :as params}]
(with-open [conn (db/open pool)]
(let [thread (db/get-by-id conn :comment-thread thread-id)]
(cmd.files/check-comment-permissions! conn profile-id (:file-id thread) share-id))
(cmd.comments/get-comments conn thread-id)))
;; --- QUERY: Get file comments users
(s/def ::file-comments-users ::cmd.comments/get-profiles-for-file-comments)
(sv/defmethod ::file-comments-users
{::doc/deprecated "1.15"
::doc/added "1.13"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id]}]
(with-open [conn (db/open pool)]
(cmd.files/check-comment-permissions! conn profile-id file-id share-id)
(cmd.comments/get-file-comments-users conn file-id profile-id)))

View File

@@ -8,38 +8,38 @@
(:require
[app.common.spec :as us]
[app.db :as db]
[app.rpc.commands.files :as files]
[app.rpc.commands.search :as search]
[app.rpc.commands.teams :as teams]
[app.rpc.commands.files :as cmd.files]
[app.rpc.commands.search :as cmd.search]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.queries.projects :as projects]
[app.rpc.queries.teams :as teams]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Query: Project Files
(s/def ::project-files ::files/get-project-files)
(s/def ::project-files ::cmd.files/get-project-files)
(sv/defmethod ::project-files
{::doc/added "1.0"
{::doc/added "1.1"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
(with-open [conn (db/open pool)]
(projects/check-read-permissions! conn profile-id project-id)
(files/get-project-files conn project-id)))
(cmd.files/get-project-files conn project-id)))
;; --- Query: File (By ID)
(s/def ::components-v2 ::us/boolean)
(s/def ::file
(s/and ::files/get-file
(s/and ::cmd.files/get-file
(s/keys :opt-un [::components-v2])))
(defn get-file
[conn id features]
(let [file (files/get-file conn id features)
thumbs (files/get-object-thumbnails conn id)]
(let [file (cmd.files/get-file conn id features)
thumbs (cmd.files/get-object-thumbnails conn id)]
(assoc file :thumbnails thumbs)))
(sv/defmethod ::file
@@ -48,19 +48,19 @@
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id id features components-v2] :as params}]
(with-open [conn (db/open pool)]
(let [perms (files/get-permissions pool profile-id id)
(let [perms (cmd.files/get-permissions pool profile-id id)
;; BACKWARD COMPATIBILTY with the components-v2 parameter
features (cond-> (or features #{})
components-v2 (conj "components/v2"))]
(files/check-read-permissions! perms)
(cmd.files/check-read-permissions! perms)
(-> (get-file conn id features)
(assoc :permissions perms)))))
;; --- QUERY: page
(s/def ::page
(s/and ::files/get-page
(s/and ::cmd.files/get-page
(s/keys :opt-un [::components-v2])))
(sv/defmethod ::page
@@ -77,18 +77,18 @@
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as params}]
(with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
(cmd.files/check-read-permissions! conn profile-id file-id)
(let [;; BACKWARD COMPATIBILTY with the components-v2 parameter
features (cond-> (or features #{})
components-v2 (conj "components/v2"))
params (assoc params :features features)]
(files/get-page conn params))))
(cmd.files/get-page conn params))))
;; --- QUERY: file-data-for-thumbnail
(s/def ::file-data-for-thumbnail
(s/and ::files/get-file-data-for-thumbnail
(s/and ::cmd.files/get-file-data-for-thumbnail
(s/keys :opt-un [::components-v2])))
(sv/defmethod ::file-data-for-thumbnail
@@ -98,18 +98,18 @@
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as props}]
(with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
(cmd.files/check-read-permissions! conn profile-id file-id)
(let [;; BACKWARD COMPATIBILTY with the components-v2 parameter
features (cond-> (or features #{})
components-v2 (conj "components/v2"))
file (files/get-file conn file-id features)]
file (cmd.files/get-file conn file-id features)]
{:file-id file-id
:revn (:revn file)
:page (files/get-file-data-for-thumbnail conn file)})))
:page (cmd.files/get-file-data-for-thumbnail conn file)})))
;; --- Query: Shared Library Files
(s/def ::team-shared-files ::files/get-team-shared-files)
(s/def ::team-shared-files ::cmd.files/get-team-shared-files)
(sv/defmethod ::team-shared-files
{::doc/added "1.3"
@@ -117,37 +117,37 @@
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(files/get-team-shared-files conn params)))
(cmd.files/get-team-shared-files conn params)))
;; --- Query: File Libraries used by a File
(s/def ::file-libraries ::files/get-file-libraries)
(s/def ::file-libraries ::cmd.files/get-file-libraries)
(sv/defmethod ::file-libraries
{::doc/added "1.3"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id features] :as params}]
(with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
(files/get-file-libraries conn file-id features)))
(cmd.files/check-read-permissions! conn profile-id file-id)
(cmd.files/get-file-libraries conn file-id features)))
;; --- Query: Files that use this File library
(s/def ::library-using-files ::files/get-library-file-references)
(s/def ::library-using-files ::cmd.files/get-library-file-references)
(sv/defmethod ::library-using-files
{::doc/added "1.13"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
(with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
(files/get-library-file-references conn file-id)))
(cmd.files/check-read-permissions! conn profile-id file-id)
(cmd.files/get-library-file-references conn file-id)))
;; --- QUERY: team-recent-files
(s/def ::team-recent-files ::files/get-team-recent-files)
(s/def ::team-recent-files ::cmd.files/get-team-recent-files)
(sv/defmethod ::team-recent-files
{::doc/added "1.0"
@@ -155,30 +155,30 @@
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(files/get-team-recent-files conn team-id)))
(cmd.files/get-team-recent-files conn team-id)))
;; --- QUERY: get file thumbnail
(s/def ::file-thumbnail ::files/get-file-thumbnail)
(s/def ::file-thumbnail ::cmd.files/get-file-thumbnail)
(sv/defmethod ::file-thumbnail
{::doc/added "1.13"
::doc/deprecated "1.17"}
[{:keys [pool]} {:keys [profile-id file-id revn]}]
(with-open [conn (db/open pool)]
(files/check-read-permissions! conn profile-id file-id)
(-> (files/get-file-thumbnail conn file-id revn)
(rph/with-http-cache files/long-cache-duration))))
(cmd.files/check-read-permissions! conn profile-id file-id)
(-> (cmd.files/get-file-thumbnail conn file-id revn)
(rph/with-http-cache cmd.files/long-cache-duration))))
;; --- QUERY: search files
(s/def ::search-files ::search/search-files)
(s/def ::search-files ::cmd.search/search-files)
(sv/defmethod ::search-files
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [pool]} {:keys [search-term] :as params}]
(when search-term
(search/search-files pool params)))
(cmd.search/search-files pool params)))

View File

@@ -9,12 +9,28 @@
[app.common.spec :as us]
[app.db :as db]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.queries.projects :as projects]
[app.rpc.queries.teams :as teams]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Query: Team Font Variants
;; TODO: deprecated, should be removed on 1.7.x
(s/def ::team-id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::team-font-variants
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::team-font-variants
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
(with-open [conn (db/open pool)]
(teams/check-read-permissions! conn profile-id team-id)
(db/query conn :team-font-variant
{:team-id team-id
:deleted-at nil})))
;; --- Query: Font Variants
(s/def ::file-id ::us/uuid)
@@ -31,7 +47,6 @@
(contains? o :project-id)))))
(sv/defmethod ::font-variants
{::doc/added "1.7"}
[{:keys [pool] :as cfg} {:keys [profile-id team-id file-id project-id] :as params}]
(with-open [conn (db/open pool)]
(cond

View File

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

View File

@@ -8,8 +8,8 @@
(:require
[app.common.spec :as us]
[app.db :as db]
[app.rpc.commands.teams :as teams]
[app.rpc.permissions :as perms]
[app.rpc.queries.teams :as teams]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))

View File

@@ -6,82 +6,244 @@
(ns app.rpc.queries.teams
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.rpc.commands.teams :as cmd.teams]
[app.rpc.doc :as-alias doc]
[app.rpc.permissions :as perms]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- Team Edition Permissions
(def ^:private sql:team-permissions
"select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
join team as t on (t.id = tpr.team_id)
where tpr.profile_id = ?
and tpr.team_id = ?
and t.deleted_at is null")
(defn get-permissions
[conn profile-id team-id]
(let [rows (db/exec! conn [sql:team-permissions profile-id team-id])
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true})))
(def has-edit-permissions?
(perms/make-edition-predicate-fn get-permissions))
(def has-read-permissions?
(perms/make-read-predicate-fn get-permissions))
(def check-edition-permissions!
(perms/make-check-fn has-edit-permissions?))
(def check-read-permissions!
(perms/make-check-fn has-read-permissions?))
;; --- Query: Teams
(s/def ::teams ::cmd.teams/get-teams)
(declare retrieve-teams)
(s/def ::profile-id ::us/uuid)
(s/def ::teams
(s/keys :req-un [::profile-id]))
(sv/defmethod ::teams
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id]}]
(with-open [conn (db/open pool)]
(cmd.teams/retrieve-teams conn profile-id)))
(retrieve-teams conn profile-id)))
(def sql:teams
"select t.*,
tp.is_owner,
tp.is_admin,
tp.can_edit,
(t.id = ?) as is_default
from team_profile_rel as tp
join team as t on (t.id = tp.team_id)
where t.deleted_at is null
and tp.profile_id = ?
order by tp.created_at asc")
(defn process-permissions
[team]
(let [is-owner (:is-owner team)
is-admin (:is-admin team)
can-edit (:can-edit team)
permissions {:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)}]
(-> team
(dissoc :is-owner :is-admin :can-edit)
(assoc :permissions permissions))))
(defn retrieve-teams
[conn profile-id]
(let [defaults (profile/retrieve-additional-data conn profile-id)]
(->> (db/exec! conn [sql:teams (:default-team-id defaults) profile-id])
(mapv process-permissions))))
;; --- Query: Team (by ID)
(s/def ::team ::cmd.teams/get-team)
(declare retrieve-team)
(s/def ::id ::us/uuid)
(s/def ::team
(s/keys :req-un [::profile-id ::id]))
(sv/defmethod ::team
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id id]}]
(with-open [conn (db/open pool)]
(cmd.teams/retrieve-team conn profile-id id)))
(retrieve-team conn profile-id id)))
(defn retrieve-team
[conn profile-id team-id]
(let [defaults (profile/retrieve-additional-data conn profile-id)
sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?")
result (db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id])]
(when-not result
(ex/raise :type :not-found
:code :team-does-not-exist))
(process-permissions result)))
;; --- Query: Team Members
(s/def ::team-members ::cmd.teams/get-team-members)
(declare retrieve-team-members)
(s/def ::team-id ::us/uuid)
(s/def ::team-members
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::team-members
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(cmd.teams/check-read-permissions! conn profile-id team-id)
(cmd.teams/retrieve-team-members conn team-id)))
(check-read-permissions! conn profile-id team-id)
(retrieve-team-members conn team-id)))
(def sql:team-members
"select tp.*,
p.id,
p.email,
p.fullname as name,
p.fullname as fullname,
p.photo_id,
p.is_active
from team_profile_rel as tp
join profile as p on (p.id = tp.profile_id)
where tp.team_id = ?")
(defn retrieve-team-members
[conn team-id]
(db/exec! conn [sql:team-members team-id]))
;; --- Query: Team Users
(s/def ::team-users ::cmd.teams/get-team-users)
(declare retrieve-users)
(declare retrieve-team-for-file)
(s/def ::file-id ::us/uuid)
(s/def ::team-users
(s/and (s/keys :req-un [::profile-id]
:opt-un [::team-id ::file-id])
#(or (:team-id %) (:file-id %))))
(sv/defmethod ::team-users
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id team-id file-id]}]
(with-open [conn (db/open pool)]
(if team-id
(do
(cmd.teams/check-read-permissions! conn profile-id team-id)
(cmd.teams/retrieve-users conn team-id))
(let [{team-id :id} (cmd.teams/retrieve-team-for-file conn file-id)]
(cmd.teams/check-read-permissions! conn profile-id team-id)
(cmd.teams/retrieve-users conn team-id)))))
(check-read-permissions! conn profile-id team-id)
(retrieve-users conn team-id))
(let [{team-id :id} (retrieve-team-for-file conn file-id)]
(check-read-permissions! conn profile-id team-id)
(retrieve-users conn team-id)))))
;; This is a similar query to team members but can contain more data
;; because some user can be explicitly added to project or file (not
;; implemented in UI)
(def sql:team-users
"select pf.id, pf.fullname, pf.photo_id
from profile as pf
inner join team_profile_rel as tpr on (tpr.profile_id = pf.id)
where tpr.team_id = ?
union
select pf.id, pf.fullname, pf.photo_id
from profile as pf
inner join project_profile_rel as ppr on (ppr.profile_id = pf.id)
inner join project as p on (ppr.project_id = p.id)
where p.team_id = ?
union
select pf.id, pf.fullname, pf.photo_id
from profile as pf
inner join file_profile_rel as fpr on (fpr.profile_id = pf.id)
inner join file as f on (fpr.file_id = f.id)
inner join project as p on (f.project_id = p.id)
where p.team_id = ?")
(def sql:team-by-file
"select p.team_id as id
from project as p
join file as f on (p.id = f.project_id)
where f.id = ?")
(defn retrieve-users
[conn team-id]
(db/exec! conn [sql:team-users team-id team-id team-id]))
(defn retrieve-team-for-file
[conn file-id]
(->> [sql:team-by-file file-id]
(db/exec-one! conn)))
;; --- Query: Team Stats
(s/def ::team-stats ::cmd.teams/get-team-stats)
(declare retrieve-team-stats)
(s/def ::team-stats
(s/keys :req-un [::profile-id ::team-id]))
(sv/defmethod ::team-stats
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(cmd.teams/check-read-permissions! conn profile-id team-id)
(cmd.teams/retrieve-team-stats conn team-id)))
(check-read-permissions! conn profile-id team-id)
(retrieve-team-stats conn team-id)))
(def sql:team-stats
"select (select count(*) from project where team_id = ?) as projects,
(select count(*) from file as f join project as p on (p.id = f.project_id) where p.team_id = ?) as files")
(defn retrieve-team-stats
[conn team-id]
(db/exec-one! conn [sql:team-stats team-id team-id]))
;; --- Query: Team invitations
(s/def ::team-invitations ::cmd.teams/get-team-invitations)
(s/def ::team-id ::us/uuid)
(s/def ::team-invitations
(s/keys :req-un [::profile-id ::team-id]))
(def sql:team-invitations
"select email_to as email, role, (valid_until < now()) as expired
from team_invitation where team_id = ? order by valid_until desc")
(sv/defmethod ::team-invitations
{::doc/added "1.0"
::doc/deprecated "1.17"}
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
(with-open [conn (db/open pool)]
(cmd.teams/check-read-permissions! conn profile-id team-id)
(cmd.teams/get-team-invitations conn team-id)))
(check-read-permissions! conn profile-id team-id)
(->> (db/exec! conn [sql:team-invitations team-id])
(mapv #(update % :role keyword)))))

View File

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

View File

@@ -5,23 +5,23 @@
;; Copyright (c) KALEIDOS INC
(ns app.rpc.retry
"A fault tolerance RPC middleware. Allow retry some operations that we
know we can retry."
"A fault tolerance helpers. Allow retry some operations that we know
we can retry."
(:require
[app.common.logging :as l]
[app.util.retry :refer [conflict-exception?]]
[app.util.services :as sv]
[promesa.core :as p]))
(defn conflict-db-insert?
"Check if exception matches a insertion conflict on postgresql."
[e]
(conflict-exception? e))
(def always-false (constantly false))
(and (instance? org.postgresql.util.PSQLException e)
(= "23505" (.getSQLState e))))
(defn wrap-retry
[_ f {:keys [::matches ::sv/name] :or {matches always-false} :as mdata}]
[_ f {:keys [::matches ::sv/name]
:or {matches (constantly false)}
:as mdata}]
(when (::enabled mdata)
(l/debug :hint "wrapping retry" :name name))
@@ -29,8 +29,8 @@
(if-let [max-retries (::max-retries mdata)]
(fn [cfg params]
(letfn [(run [retry]
(->> (f cfg params)
(p/merr (partial handle-error retry))))
(-> (f cfg params)
(p/catch (partial handle-error retry))))
(handle-error [retry cause]
(if (matches cause)
@@ -40,6 +40,6 @@
(run current-retry)
(throw cause)))
(throw cause)))]
(run 1)))
(run 0)))
f))

View File

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

View File

@@ -1,41 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.setup.initial-user
"Initial data setup of instance."
(:require
[app.auth :as auth]
[app.common.logging :as l]
[app.common.uuid :as uuid]
[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,7 +8,6 @@
"A main namespace for server repl."
#_:clj-kondo/ignore
(:require
[app.auth :refer [derive-password]]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l]
@@ -21,6 +20,7 @@
[app.db :as db]
[app.db.sql :as sql]
[app.main :refer [system]]
[app.rpc.commands.auth :refer [derive-password]]
[app.rpc.queries.profile :as prof]
[app.util.blob :as blob]
[app.util.time :as dt]

View File

@@ -6,7 +6,7 @@
(ns app.tasks.objects-gc
"A maintenance task that performs a general purpose garbage collection
of deleted or unreachable objects."
of deleted objects."
(:require
[app.common.data :as d]
[app.common.logging :as l]
@@ -16,247 +16,154 @@
[app.storage :as sto]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig]))
(declare ^:private delete-profiles!)
(declare ^:private delete-teams!)
(declare ^:private delete-fonts!)
(declare ^:private delete-projects!)
(declare ^:private delete-files!)
(declare ^:private delete-orphan-teams!)
(def target-tables
["profile"
"team"
"file"
"project"
"team_font_variant"])
(defmulti delete-objects :table)
(def sql:delete-objects
"with deleted as (
select id from %(table)s
where deleted_at is not null
and deleted_at < now() - ?::interval
order by deleted_at
limit %(limit)s
)
delete from %(table)s
where id in (select id from deleted)
returning *")
;; --- IMPL: generic object deletion
(defmethod delete-objects :default
[{:keys [conn min-age table] :as cfg}]
(let [sql (str/fmt sql:delete-objects
{:table table :limit 50})
result (db/exec! conn [sql min-age])]
(doseq [{:keys [id] :as item} result]
(l/debug :hint "permanently delete object" :table table :id id))
(count result)))
;; --- IMPL: file deletion
(defmethod delete-objects "file"
[{:keys [conn min-age table] :as cfg}]
(let [sql (str/fmt sql:delete-objects {:table table :limit 50})
result (db/exec! conn [sql min-age])]
(doseq [{:keys [id] :as item} result]
(l/debug :hint "permanently delete object" :table table :id id))
(count result)))
;; --- IMPL: team-font-variant deletion
(defmethod delete-objects "team_font_variant"
[{:keys [conn min-age storage table] :as cfg}]
(let [sql (str/fmt sql:delete-objects {:table table :limit 50})
fonts (db/exec! conn [sql min-age])
storage (media/configure-assets-storage storage conn)]
(doseq [{:keys [id] :as font} fonts]
(l/debug :hint "permanently delete object" :table table :id id)
(some->> (:woff1-file-id font) (sto/touch-object! storage) deref)
(some->> (:woff2-file-id font) (sto/touch-object! storage) deref)
(some->> (:otf-file-id font) (sto/touch-object! storage) deref)
(some->> (:ttf-file-id font) (sto/touch-object! storage) deref))
(count fonts)))
;; --- IMPL: team deletion
(defmethod delete-objects "team"
[{:keys [conn min-age storage table] :as cfg}]
(let [sql (str/fmt sql:delete-objects {:table table :limit 50})
teams (db/exec! conn [sql min-age])
storage (media/configure-assets-storage storage conn)]
(doseq [{:keys [id] :as team} teams]
(l/debug :hint "permanently delete object" :table table :id id)
(some->> (:photo-id team) (sto/touch-object! storage) deref))
(count teams)))
;; --- IMPL: profile deletion
(def sql:retrieve-deleted-profiles
"select id, photo_id from profile
where deleted_at is not null
and deleted_at < now() - ?::interval
order by deleted_at
limit ?
for update")
(defmethod delete-objects "profile"
[{:keys [conn min-age storage table] :as cfg}]
(let [profiles (db/exec! conn [sql:retrieve-deleted-profiles min-age 50])
storage (media/configure-assets-storage storage conn)]
(doseq [{:keys [id] :as profile} profiles]
(l/debug :hint "permanently delete object" :table table :id id)
;; Mark as deleted the storage object related with the photo-id
;; field.
(some->> (:photo-id profile) (sto/touch-object! storage) deref)
;; And finally, permanently delete the profile.
(db/delete! conn :profile {:id id}))
(count profiles)))
;; --- INIT
(defn- process-table
[{:keys [table] :as cfg}]
(loop [n 0]
(let [res (delete-objects cfg)]
(if (pos? res)
(recur (+ n res))
(do
(l/debug :hint "delete summary" :table table :total n)
n)))))
(s/def ::min-age ::dt/duration)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool ::sto/storage]
:opt [::min-age]))
(s/keys :req-un [::db/pool ::sto/storage]
:opt-un [::min-age]))
(defmethod ig/prep-key ::handler
[_ cfg]
(merge {::min-age cf/deletion-delay}
(merge {:min-age cf/deletion-delay}
(d/without-nils cfg)))
(defmethod ig/init-key ::handler
[_ {:keys [::db/pool ::sto/storage] :as cfg}]
[_ {:keys [pool] :as cfg}]
(fn [params]
(db/with-atomic [conn pool]
(let [min-age (or (:min-age params) (::min-age cfg))
_ (l/info :hint "gc started"
:min-age (dt/format-duration min-age)
:rollback? (boolean (:rollback? params)))
storage (media/configure-assets-storage storage conn)
(let [min-age (or (:min-age params) (:min-age cfg))
cfg (-> cfg
(assoc ::min-age (db/interval min-age))
(assoc ::conn conn)
(assoc ::storage storage))
(assoc :min-age (db/interval min-age))
(assoc :conn conn))]
(loop [tables (seq target-tables)
total 0]
(if-let [table (first tables)]
(recur (rest tables)
(+ total (process-table (assoc cfg :table table))))
(do
(l/info :hint "objects gc finished successfully"
:min-age (dt/format-duration min-age)
:total total)
htotal (+ (delete-profiles! cfg)
(delete-teams! cfg)
(delete-projects! cfg)
(delete-files! cfg)
(delete-fonts! cfg))
stotal (delete-orphan-teams! cfg)]
(when (:rollback? params)
(db/rollback! conn))
(l/info :hint "gc finished"
:deleted htotal
:orphans stotal
:rollback? (boolean (:rollback? params)))
{:processed total})))))))
(when (:rollback? params)
(db/rollback! conn))
{:processed (+ stotal htotal)}))))
(def ^:private sql:get-profiles-chunk
"select id, photo_id, created_at from profile
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(defn- delete-profiles!
[{:keys [::conn ::min-age ::storage] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-profiles-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))]
(reduce
(fn [total {:keys [id photo-id]}]
(l/debug :hint "permanently delete profile" :id (str id))
;; Mark as deleted the storage object related with the
;; photo-id field.
(some->> photo-id (sto/touch-object! storage) deref)
;; And finally, permanently delete the profile.
(db/delete! conn :profile {:id id})
(inc total))
0
(d/iteration get-chunk
:vf second
:kf first
:initk (dt/now)))))
(def ^:private sql:get-teams-chunk
"select id, photo_id, created_at from team
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(defn- delete-teams!
[{:keys [::conn ::min-age ::storage] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-teams-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))]
(reduce
(fn [total {:keys [id photo-id]}]
(l/debug :hint "permanently delete team" :id (str id))
;; Mark as deleted the storage object related with the
;; photo-id field.
(some->> photo-id (sto/touch-object! storage) deref)
;; And finally, permanently delete the team.
(db/delete! conn :team {:id id})
(inc total))
0
(d/iteration get-chunk
:vf second
:kf first
:initk (dt/now)))))
(def ^:private sql:get-orphan-teams-chunk
"select t.id, t.created_at
from team as t
left join team_profile_rel as tpr
on (t.id = tpr.team_id)
where tpr.profile_id is null
and t.created_at < ?
order by t.created_at desc
limit 10
for update of t skip locked;")
(defn- delete-orphan-teams!
"Find all orphan teams (with no members and mark them for
deletion (soft delete)."
[{:keys [::conn] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-orphan-teams-chunk cursor])]
[(some->> rows peek :created-at) rows]))]
(reduce
(fn [total {:keys [id]}]
(l/debug :hint "mark team for deletion" :id (str id))
;; And finally, permanently delete the team.
(db/update! conn :team
{:deleted-at (dt/now)}
{:id id})
(inc total))
0
(d/iteration get-chunk
:vf second
:kf first
:initk (dt/now)))))
(def ^:private sql:get-fonts-chunk
"select id, created_at, woff1_file_id, woff2_file_id, otf_file_id, ttf_file_id
from team_font_variant
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(defn- delete-fonts!
[{:keys [::conn ::min-age ::storage] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-fonts-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))]
(reduce
(fn [total {:keys [id] :as font}]
(l/debug :hint "permanently delete font variant" :id (str id))
;; Mark as deleted the all related storage objects
(some->> (:woff1-file-id font) (sto/touch-object! storage) deref)
(some->> (:woff2-file-id font) (sto/touch-object! storage) deref)
(some->> (:otf-file-id font) (sto/touch-object! storage) deref)
(some->> (:ttf-file-id font) (sto/touch-object! storage) deref)
;; And finally, permanently delete the team font variant
(db/delete! conn :team-font-variant {:id id})
(inc total))
0
(d/iteration get-chunk
:vf second
:kf first
:initk (dt/now)))))
(def ^:private sql:get-projects-chunk
"select id, created_at
from project
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(defn- delete-projects!
[{:keys [::conn ::min-age] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-projects-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))]
(reduce
(fn [total {:keys [id]}]
(l/debug :hint "permanently delete project" :id (str id))
;; And finally, permanently delete the project.
(db/delete! conn :project {:id id})
(inc total))
0
(d/iteration get-chunk
:vf second
:kf first
:initk (dt/now)))))
(def ^:private sql:get-files-chunk
"select id, created_at
from file
where deleted_at is not null
and deleted_at < now() - ?::interval
and created_at < ?
order by created_at desc
limit 10
for update skip locked")
(defn- delete-files!
[{:keys [::conn ::min-age] :as cfg}]
(letfn [(get-chunk [cursor]
(let [rows (db/exec! conn [sql:get-files-chunk min-age cursor])]
[(some->> rows peek :created-at) rows]))]
(reduce
(fn [total {:keys [id]}]
(l/debug :hint "permanently delete file" :id (str id))
;; And finally, permanently delete the file.
(db/delete! conn :file {:id id})
(inc total))
0
(d/iteration get-chunk
:vf second
:kf first
:initk (dt/now)))))

View File

@@ -1,34 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.util.retry
"A fault tolerance helpers. Allow retry some operations that we know
we can retry."
(:require
[app.common.logging :as l])
(:import
org.postgresql.util.PSQLException))
(defn conflict-exception?
"Check if exception matches a insertion conflict on postgresql."
[e]
(and (instance? PSQLException e)
(= "23505" (.getSQLState ^PSQLException e))))
(defmacro with-retry
[{:keys [::when ::max-retries ::label] :or {max-retries 3}} & body]
`(loop [tnum# 1]
(let [result# (try
~@body
(catch Throwable cause#
(if (and (~when cause#) (<= tnum# ~max-retries))
::retry
(throw cause#))))]
(if (= ::retry result#)
(do
(l/warn :hint "retrying operation" :label ~label)
(recur (inc tnum#)))
result#))))

View File

@@ -217,7 +217,7 @@
(l/debug :hist "dispatcher: queue tasks"
:queue queue
:tasks (count ids)
:queued res)))
:total-queued res)))
(run-batch! [rconn]
(db/with-atomic [conn pool]
@@ -446,11 +446,10 @@
:else
(try
(l/debug :hint "worker: executing task"
:name (:name task)
:id (:id task)
:queue queue
:worker-id worker-id
:retry (:retry-num task))
:task-id (:id task)
:task-name (:name task)
:task-retry (:retry-num task))
(handle-task task)
(catch InterruptedException cause
(throw cause))

View File

@@ -6,7 +6,6 @@
(ns backend-tests.helpers
(:require
[app.auth]
[app.common.data :as d]
[app.common.flags :as flags]
[app.common.pages :as cp]
@@ -18,14 +17,14 @@
[app.main :as main]
[app.media]
[app.migrations]
[app.rpc :as-alias rpc]
[app.rpc.helpers :as rph]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.commands.files :as files]
[app.rpc.commands.files.create :as files.create]
[app.rpc.commands.files.update :as files.update]
[app.rpc.commands.teams :as teams]
[app.rpc.helpers :as rph]
[app.rpc.mutations.profile :as profile]
[app.rpc.mutations.projects :as projects]
[app.rpc.mutations.teams :as teams]
[app.util.blob :as blob]
[app.util.services :as sv]
[app.util.time :as dt]
@@ -103,9 +102,8 @@
*pool* (:app.db/pool system)]
(with-redefs [app.config/flags (flags/parse flags/default default-flags (:flags config))
app.config/config config
app.loggers.audit/submit! (constantly nil)
app.auth/derive-password identity
app.auth/verify-password (fn [a b] {:valid (= a b)})]
app.rpc.commands.auth/derive-password identity
app.rpc.commands.auth/verify-password (fn [a b] {:valid (= a b)})]
(next)))
(finally
(ig/halt! system)))))
@@ -174,7 +172,7 @@
(->> (merge {:id (mk-uuid "project" i)
:name (str "project" i)}
params)
(#'teams/create-project conn)))))
(#'projects/create-project conn)))))
(defn create-file*
([i params]
@@ -256,7 +254,7 @@
([params] (create-project-role* *pool* params))
([pool {:keys [project-id profile-id role] :or {role :owner}}]
(with-open [conn (db/open pool)]
(#'teams/create-project-role conn {:project-id project-id
(#'projects/create-project-role conn {:project-id project-id
:profile-id profile-id
:role role}))))
@@ -325,21 +323,14 @@
(try-on! (method-fn (dissoc data ::type)))))
(defn mutation!
[{:keys [::type profile-id] :as data}]
[{:keys [::type] :as data}]
(let [method-fn (get-in *system* [:app.rpc/methods :mutations type])]
(try-on! (method-fn (-> data
(dissoc ::type)
(assoc ::rpc/profile-id profile-id)
(d/without-nils))))))
(try-on! (method-fn (dissoc data ::type)))))
(defn query!
[{:keys [::type profile-id] :as data}]
[{:keys [::type] :as data}]
(let [method-fn (get-in *system* [:app.rpc/methods :queries type])]
(try-on! (method-fn (-> data
(dissoc ::type)
(assoc ::rpc/profile-id profile-id)
(d/without-nils))))))
(try-on! (method-fn (dissoc data ::type)))))
(defn run-task!
([name]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,14 +6,13 @@
(ns backend-tests.rpc-team-test
(:require
[backend-tests.helpers :as th]
[app.common.uuid :as uuid]
[app.db :as db]
[app.http :as http]
[app.rpc :as-alias rpc]
[app.storage :as sto]
[app.tokens :as tokens]
[app.util.time :as dt]
[backend-tests.helpers :as th]
[clojure.test :as t]
[datoteka.core :as fs]
[mockery.core :refer [with-mocks]]))
@@ -64,16 +63,6 @@
(t/is (th/success? out))
(t/is (= 1 (:call-count (deref mock)))))
;; get invitation token
(let [params {::th/type :get-team-invitation-token
::rpc/profile-id (:id profile1)
:team-id (:id team)
:email "foo@bar.com"}
out (th/command! params)]
(t/is (th/success? out))
(let [result (:result out)]
(contains? result :token)))
;; invite user with bounce
(th/reset-mock! mock)
@@ -190,7 +179,7 @@
:valid-until (dt/in-future "48h")})
(let [data {::th/type :verify-token :token token}
out (th/command! data)]
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
@@ -215,8 +204,8 @@
:role "editor"
:valid-until (dt/in-future "48h")})
(let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile2)}
out (th/command! data)]
(let [data {::th/type :verify-token :token token :profile-id (:id profile2)}
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (th/success? out))
(let [result (:result out)]
@@ -236,8 +225,8 @@
:role "editor"
:valid-until (dt/in-future "48h")})
(let [data {::th/type :verify-token :token token ::rpc/profile-id (:id profile1)}
out (th/command! data)]
(let [data {::th/type :verify-token :token token :profile-id (:id profile1)}
out (th/mutation! data)]
;; (th/print-result! out)
(t/is (not (th/success? out)))
(let [edata (-> out :error ex-data)]
@@ -246,6 +235,8 @@
)))
(t/deftest invite-team-member-with-email-verification-disabled
(with-mocks [mock {:target 'app.emails/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})

View File

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

View File

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

View File

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

View File

@@ -58,6 +58,21 @@
[shape]
(or (:y shape) (:y (:selrect shape)))) ; Paths don't have :y attribute
(defn orig-pos
"Return the top left point of the shape wrapper BEFORE applying transformations."
[shape]
(gpt/point (left-bound shape) (top-bound shape)))
(defn width
"Return the width of the shape BEFORE transformations."
[shape]
(-> shape :selrect :width))
(defn height
"Return the height of the shape BEFORE transformations."
[shape]
(-> shape :selrect :height))
(defn fully-contained?
"Checks if one rect is fully inside the other"
[rect other]

View File

@@ -6,6 +6,7 @@
(ns app.common.geom.shapes.constraints
(:require
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.intersect :as gsi]
[app.common.geom.shapes.points :as gpo]
@@ -183,7 +184,7 @@
(ctm/move-modifiers (displacement end-before end-after))))
(defmethod constraint-modifier :fixed
[_ axis child-points-before parent-points-before child-points-after parent-points-after]
[_ axis child-points-before parent-points-before child-points-after parent-points-after {:keys [transform transform-inverse]} modifiers]
(let [;; Same as constraint end
end-before (end-vector axis child-points-before parent-points-before)
end-after (end-vector axis child-points-after parent-points-after)
@@ -204,11 +205,14 @@
resize-origin (gpo/origin child-points-after)
[_ transform transform-inverse] (gtr/calculate-geometry parent-points-after)
modif-transform (ctm/modifiers->transform modifiers)
modif-transform-inverse (gmt/inverse modif-transform)
resize-transform (gmt/multiply modif-transform transform)
resize-transform-inverse (gmt/multiply transform-inverse modif-transform-inverse)
resize-vector (get-scale axis scale)]
(-> (ctm/empty)
(ctm/resize resize-vector resize-origin transform transform-inverse)
(ctm/resize resize-vector resize-origin resize-transform resize-transform-inverse)
(ctm/move disp-start))))
(defmethod constraint-modifier :center
@@ -249,6 +253,7 @@
(defn normalize-modifiers
"Before aplying constraints we need to remove the deformation caused by the resizing of the parent"
[constraints-h constraints-v modifiers
{:keys [transform transform-inverse] :as parent}
child-bounds transformed-child-bounds parent-bounds transformed-parent-bounds]
(let [child-bb-before (gpo/parent-coords-bounds child-bounds parent-bounds)
@@ -263,11 +268,17 @@
(/ (gpo/height-points child-bb-before) (gpo/height-points child-bb-after)))
resize-vector (gpt/point scale-x scale-y)
resize-origin (gpo/origin transformed-child-bounds)
[_ transform transform-inverse] (gtr/calculate-geometry transformed-parent-bounds)]
modif-transform (ctm/modifiers->transform modifiers)
modif-transform-inverse (gmt/inverse modif-transform)
resize-transform (gmt/multiply modif-transform transform)
resize-transform-inverse (gmt/multiply transform-inverse modif-transform-inverse)
resize-origin (gpo/origin transformed-child-bounds)]
(-> modifiers
(ctm/resize resize-vector resize-origin transform transform-inverse))))
(ctm/resize
resize-vector
resize-origin
resize-transform
resize-transform-inverse))))
(defn calc-child-modifiers
[parent child modifiers ignore-constraints child-bounds parent-bounds transformed-parent-bounds]
@@ -303,7 +314,8 @@
modifiers (ctm/select-child modifiers)
transformed-child-bounds (gtr/transform-bounds child-bounds modifiers)
modifiers (normalize-modifiers constraints-h constraints-v modifiers
modifiers (normalize-modifiers constraints-h constraints-v
modifiers parent
child-bounds transformed-child-bounds parent-bounds transformed-parent-bounds)
transformed-child-bounds (gtr/transform-bounds child-bounds modifiers)
@@ -312,11 +324,13 @@
modifiers-h (constraint-modifier (constraints-h const->type+axis) :x
child-points-before parent-bounds
child-points-after transformed-parent-bounds)
child-points-after transformed-parent-bounds
parent modifiers)
modifiers-v (constraint-modifier (constraints-v const->type+axis) :y
child-points-before parent-bounds
child-points-after transformed-parent-bounds)]
child-points-after transformed-parent-bounds
parent modifiers)]
(-> modifiers
(ctm/add-modifiers modifiers-h)
(ctm/add-modifiers modifiers-v))))))

View File

@@ -9,15 +9,12 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes.flex-layout.positions :as fpo]
[app.common.geom.shapes.points :as gpo]
[app.common.geom.shapes.transforms :as gtr]
[app.common.types.modifiers :as ctm]
[app.common.types.shape.layout :as ctl]))
(defn calc-fill-width-data
"Calculates the size and modifiers for the width of an auto-fill child"
[parent
transform
transform-inverse
[{:keys [transform transform-inverse] :as parent}
child
child-origin child-width
{:keys [children-data line-width] :as layout-data}]
@@ -39,8 +36,7 @@
(defn calc-fill-height-data
"Calculates the size and modifiers for the height of an auto-fill child"
[parent
transform transform-inverse
[{:keys [transform transform-inverse] :as parent}
child
child-origin child-height
{:keys [children-data line-height] :as layout-data}]
@@ -62,17 +58,13 @@
(defn layout-child-modifiers
"Calculates the modifiers for the layout"
[parent parent-bounds child child-bounds layout-line]
[parent child child-bounds layout-line]
(let [child-origin (gpo/origin child-bounds)
child-width (gpo/width-points child-bounds)
child-height (gpo/height-points child-bounds)
[_ transform transform-inverse]
(when (or (ctl/fill-width? child) (ctl/fill-width? child))
(gtr/calculate-geometry @parent-bounds))
fill-width (when (ctl/fill-width? child) (calc-fill-width-data parent transform transform-inverse child child-origin child-width layout-line))
fill-height (when (ctl/fill-height? child) (calc-fill-height-data parent transform transform-inverse child child-origin child-height layout-line))
fill-width (when (ctl/fill-width? child) (calc-fill-width-data parent child child-origin child-width layout-line))
fill-height (when (ctl/fill-height? child) (calc-fill-height-data parent child child-origin child-height layout-line))
child-width (or (:width fill-width) child-width)
child-height (or (:height fill-height) child-height)
@@ -86,4 +78,5 @@
(cond-> fill-width (ctm/add-modifiers (:modifiers fill-width)))
(cond-> fill-height (ctm/add-modifiers (:modifiers fill-height)))
(ctm/move move-vec))]
[modifiers layout-line]))

View File

@@ -160,7 +160,7 @@
(set-child-modifiers [[layout-line modif-tree] [child-bounds child]]
(let [[modifiers layout-line]
(gcl/layout-child-modifiers parent transformed-parent-bounds child child-bounds layout-line)
(gcl/layout-child-modifiers parent child child-bounds layout-line)
modif-tree
(cond-> modif-tree

View File

@@ -12,19 +12,16 @@
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.points :as gpo]
[app.common.geom.shapes.rect :as gpr]
[app.common.geom.shapes.transforms :as gtr]
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.types.modifiers :as ctm]))
(defn size-pixel-precision
[modifiers shape points]
[modifiers {:keys [transform transform-inverse] :as shape} points]
(let [origin (gpo/origin points)
curr-width (gpo/width-points points)
curr-height (gpo/height-points points)
[_ transform transform-inverse] (gtr/calculate-geometry points)
path? (cph/path-shape? shape)
vertical-line? (and path? (<= curr-width 0.01))
horizontal-line? (and path? (<= curr-height 0.01))

View File

@@ -22,9 +22,7 @@
(let [points (->> shape
:position-data
(mapcat (comp gpr/rect->points position-data->rect)))]
(if (empty? points)
(:selrect shape)
(-> points (gpr/points->selrect)))))
(-> points (gpr/points->selrect))))
(defn position-data-bounding-box
[shape]

View File

@@ -44,9 +44,8 @@
(defn build-message-cause
[props]
#?(:clj (when-let [[_ cause] (d/seek (fn [[k]] (= k :cause)) props)]
(when cause
(with-out-str
(ex/print-throwable cause))))
(with-out-str
(ex/print-throwable cause)))
:cljs nil))
(defn build-message

View File

@@ -16,6 +16,7 @@
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.pages.changes-spec :as pcs]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.colors-list :as ctcl]
@@ -116,7 +117,7 @@
(cond-> parent
(and (:shape-ref parent)
(not ignore-touched))
(-> (update :touched cph/set-touched-group :shapes-group)
(-> (update :touched ctk/set-touched-group :shapes-group)
(dissoc :remote-synced?)))))
(delete-from-objects [objects]
@@ -221,7 +222,7 @@
(update :shapes d/vec-without-nils))]
(cond-> parent
(and (:shape-ref parent) (= (:type parent) :group) (not ignore-touched))
(-> (update :touched cph/set-touched-group :shapes-group)
(-> (update :touched ctk/set-touched-group :shapes-group)
(dissoc :remote-synced?)))))
(remove-from-old-parent [old-objects objects shape-id]
@@ -241,7 +242,7 @@
(d/update-in-when [pid :shapes] without-obj sid)
(d/update-in-when [pid :shapes] d/vec-without-nils)
(cond-> component? (d/update-when pid #(-> %
(update :touched cph/set-touched-group :shapes-group)
(update :touched ctk/set-touched-group :shapes-group)
(dissoc :remote-synced?)))))))))
(update-parent-id [objects id]
@@ -416,7 +417,7 @@
(not root-name?)
(not (and ignore-geometry is-geometry?)))
(->
(update :touched cph/set-touched-group group)
(update :touched ctk/set-touched-group group)
(dissoc :remote-synced?))
(nil? val)
@@ -459,7 +460,7 @@
(defmulti components-changed (fn [_ change] (:type change)))
(defmethod components-changed :mod-obj
[file-data {:keys [id page-id _component-id operations]}]
[file-data {:keys [id page-id operations]}]
(when page-id
(let [page (ctpl/get-page file-data page-id)
shape-and-parents (map #(ctn/get-shape page %)

View File

@@ -15,6 +15,7 @@
[app.common.math :as mth]
[app.common.pages :as cp]
[app.common.pages.helpers :as cph]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]))
@@ -43,7 +44,7 @@
(defn with-container
[changes container]
(if (cph/page? container)
(if (ctn/page? container)
(vary-meta changes assoc ::page-id (:id container))
(vary-meta changes assoc ::component-id (:id container))))

View File

@@ -50,7 +50,6 @@
:r2 :radius-group
:r3 :radius-group
:r4 :radius-group
:type :geometry-group
:selrect :geometry-group
:points :geometry-group
:locked :geometry-group
@@ -91,7 +90,8 @@
:layout-item-min-h :layout-item
:layout-item-max-w :layout-item
:layout-item-min-w :layout-item
:layout-item-align-self :layout-item})
:layout-item-align-self :layout-item
})
;; Attributes that may directly be edited by the user with forms
(def editable-attrs

View File

@@ -190,71 +190,6 @@
:else
(recur (get-in objects [current-id :parent-id])))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COMPONENTS HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn set-touched-group
[touched group]
(conj (or touched #{}) group))
(defn touched-group?
[shape group]
((or (:touched shape) #{}) group))
(defn get-component
"Retrieve a component from libraries, if no library-id is provided, we
iterate over all libraries and find the component on it."
([libraries component-id]
(some #(-> % :data :components (get component-id)) (vals libraries)))
([libraries library-id component-id]
(get-in libraries [library-id :data :components component-id])))
(defn get-component-shape
"Get the parent shape linked to a component for this shape, if any"
[objects shape]
(if-not (:shape-ref shape)
nil
(if (:component-id shape)
shape
(if-let [parent-id (:parent-id shape)]
(get-component-shape objects (get objects parent-id))
nil))))
(defn get-root-shape
"Get the root shape linked to a component for this shape, if any."
[objects shape]
(cond
(some? (:component-root? shape))
shape
(some? (:shape-ref shape))
(recur objects (get objects (:parent-id shape)))))
(defn make-container
[page-or-component type]
(assoc page-or-component :type type))
(defn page?
[container]
(= (:type container) :page))
(defn component?
[container]
(= (:type container) :component))
(defn get-container
[file type id]
(us/assert map? file)
(us/assert keyword? type)
(us/assert uuid? id)
(-> (if (= type :page)
(get-in file [:pages-index id])
(get-in file [:components id]))
(assoc :type type)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ALGORITHMS & TRANSFORMATIONS FOR SHAPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -135,7 +135,7 @@
(letfn [(conformer [s]
(cond
(u/uri? s) s
(string? s) (u/uri (str/trim s))
(string? s) (u/uri s)
:else ::s/invalid))
(unformer [v]
(dm/str v))]

View File

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

View File

@@ -7,10 +7,23 @@
(ns app.common.types.component)
(defn instance-root?
"Check if the shape is the root of an instance or a subinstance."
[shape]
(some? (:component-id shape)))
(defn instance-tree-root?
"Check if the shape is the root of an instance that is no
subinstance of a higher one."
[shape]
(:component-root? shape))
(defn instance-shape?
"Check if the shape is part of any instance."
[shape]
(some? (:shape-ref shape)))
(defn instance-of?
"Check if the shape is the root of a near instance of the component."
[shape file-id component-id]
(and (some? (:component-id shape))
(some? (:component-file shape))
@@ -18,8 +31,10 @@
(= (:component-file shape) file-id)))
(defn is-main-of?
"Check if the first shape is the near main of the second one."
[shape-main shape-inst]
(and (:shape-ref shape-inst)
(and (not= (:id shape-main) (:id shape-inst))
(:shape-ref shape-inst)
(or (= (:shape-ref shape-inst) (:id shape-main))
(= (:shape-ref shape-inst) (:shape-ref shape-main)))))
@@ -35,11 +50,13 @@
(= page-id (:main-instance-page component))))
(defn get-component-root
"Get the root shape of the component."
[component]
(get-in component [:objects (:id component)]))
(defn uses-library-components?
"Check if the shape uses any component in the given library."
"Check if the shape is the root of an instance of any component in
the given library."
[shape library-id]
(and (some? (:component-id shape))
(= (:component-file shape) library-id)))
@@ -49,3 +66,13 @@
[shape]
(some? (:shape-ref shape)))
(defn set-touched-group
"Add a group to the touched flags."
[touched group]
(conj (or touched #{}) group))
(defn touched-group?
"Check if the touched flags contain the given group."
[shape group]
((or (:touched shape) #{}) group))

View File

@@ -10,6 +10,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.spec :as us]
[app.common.types.component :as ctk]
[app.common.types.shape-tree :as ctst]
[clojure.spec.alpha :as s]))
@@ -61,6 +62,40 @@
[container shape-id f]
(update-in container [:objects shape-id] f))
(defn get-component-shape
"Get the root shape of an instance, the one that is linked to the component.
If this is a subinstance, get the most direct root."
[objects shape]
(if-not (:shape-ref shape)
nil
(if (:component-id shape)
shape
(if-let [parent-id (:parent-id shape)]
(get-component-shape objects (get objects parent-id))
nil))))
(defn get-root-shape
"Get the topmost root shape of an instance, the one that is linked to the
component and without any container instance upwards."
[objects shape]
(cond
(some? (:component-root? shape))
shape
(some? (:shape-ref shape))
(recur objects (get objects (:parent-id shape)))))
(defn get-instances
"Get all shapes in the objects list that are near instances of the given one
---------------------------------------------------------------------------
TODO: Warning!!! this is a slow operation, since it needs to walk the whole
objects list. Perhaps there is a way of indexing this someway.
---------------------------------------------------------------------------"
[objects main-shape]
(filter #(ctk/is-main-of? main-shape %)
(vals objects)))
(defn make-component-shape
"Clone the shape and all children. Generate new ids and detach
from parent and frame. Update the original shapes to have links

View File

@@ -6,6 +6,7 @@
(ns app.common.types.file
(:require
[app.common.pprint :refer [pprint]]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
@@ -13,6 +14,7 @@
[app.common.geom.shapes :as gsh]
[app.common.pages.common :refer [file-version]]
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.types.color :as ctc]
[app.common.types.colors-list :as ctcl]
[app.common.types.component :as ctk]
@@ -99,6 +101,17 @@
(concat (map #(ctn/make-container % :page) (ctpl/pages-seq file-data))
(map #(ctn/make-container % :component) (ctkl/components-seq file-data))))
(defn get-container
[file type id]
(us/assert map? file)
(us/assert :app.common.types.container/type type)
(us/assert uuid? id)
(-> (if (= type :page)
(get-in file [:pages-index id])
(get-in file [:components id]))
(assoc :type type)))
(defn update-container
"Update a container inside the file, it can be a page or a component"
[file-data container f]
@@ -500,7 +513,7 @@
(show-component [shape objects]
(if (nil? (:shape-ref shape))
""
(let [root-shape (cph/get-component-shape objects shape)
(let [root-shape (ctn/get-component-shape objects shape)
component-id (when root-shape (:component-id root-shape))
component-file-id (when root-shape (:component-file root-shape))
component-file (when component-file-id (get libraries component-file-id nil))

View File

@@ -304,11 +304,6 @@
(-> (or modifiers (empty))
(update :structure-child conj (change-property-op property value))))
(defn change-parent-property
[modifiers property value]
(-> (or modifiers (empty))
(update :structure-parent conj (change-property-op property value))))
(defn- concat-geometry
[operations other merge?]

View File

@@ -13,6 +13,7 @@
[app.common.math :as mth]
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.types.component :as ctk]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[clojure.spec.alpha :as s]))
@@ -45,7 +46,7 @@
(cond-> (and (:shape-ref parent)
(not= (:id parent) frame-id)
(not ignore-touched))
(-> (update :touched cph/set-touched-group :shapes-group)
(-> (update :touched ctk/set-touched-group :shapes-group)
(dissoc :remote-synced?)))))
;; TODO: this looks wrong, why we allow nil values?

View File

@@ -9,7 +9,8 @@
[clojure.test :as t]
[app.common.pages.helpers :as cph]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]))
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]))
;; ---- Helpers to manage libraries and synchronization
@@ -81,7 +82,7 @@
[page root-inst-id libraries]
(let [root-inst (ctn/get-shape page root-inst-id)
component (cph/get-component libraries (:component-id root-inst))
component (ctf/get-component libraries (:component-id root-inst))
shapes-inst (cph/get-children-with-self (:objects page) root-inst-id)
shapes-main (cph/get-children-with-self (:objects component) (:shape-ref root-inst))
@@ -90,10 +91,10 @@
main-exists? (fn [shape]
(let [component-shape
(cph/get-component-shape (:objects page) shape)
(ctn/get-component-shape (:objects page) shape)
component
(cph/get-component libraries (:component-id component-shape))
(ctf/get-component libraries (:component-id component-shape))
main-shape
(ctn/get-shape component (:shape-ref shape))]
@@ -117,7 +118,7 @@
[page root-inst-id libraries]
(let [root-inst (ctn/get-shape page root-inst-id)
component (cph/get-component libraries (:component-id root-inst))
component (ctf/get-component libraries (:component-id root-inst))
shapes-inst (cph/get-children-with-self (:objects page) root-inst-id)
shapes-main (cph/get-children-with-self (:objects component) (:shape-ref root-inst))
@@ -126,10 +127,10 @@
main-exists? (fn [shape]
(let [component-shape
(cph/get-component-shape (:objects page) shape)
(ctn/get-component-shape (:objects page) shape)
component
(cph/get-component libraries (:component-id component-shape))
(ctf/get-component libraries (:component-id component-shape))
main-shape
(ctn/get-shape component (:shape-ref shape))]
@@ -144,7 +145,7 @@
(defn resolve-component
"Get the component with the given id and all its shapes."
[page component-id libraries]
(let [component (cph/get-component libraries component-id)
(let [component (ctf/get-component libraries component-id)
root-main (ctk/get-component-root component)
shapes-main (cph/get-children-with-self (:objects component) (:id root-main))]

View File

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

View File

@@ -1,68 +1,11 @@
FROM ubuntu:22.04 as jre-build
ENV DEBIAN_FRONTEND=noninteractive \
TZ=Etc/UTC
RUN set -eux; \
apt-get -qq update; \
apt-get -qqy --no-install-recommends install \
curl \
ca-certificates \
binutils \
; \
rm -rf /var/lib/apt/lists/*;
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='262be608e266fd76d7496af83b2832be853c3aaf7460d6a4da198cd40db74553'; \
BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.2.1%2B1/OpenJDK18U-jdk_aarch64_linux_hotspot_18.0.2.1_1.tar.gz'; \
;; \
armhf|armv7l) \
ESUM='4cd49b92d13847bfad7b3bf635cca349e2c89c7641748c5288bc40d612cdbbd6'; \
BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.2.1%2B1/OpenJDK18U-jdk_arm_linux_hotspot_18.0.2.1_1.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='7d6beba8cfc0a8347f278f7414351191a95a707d46b6586e9a786f2669af0f8b'; \
BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.2.1%2B1/OpenJDK18U-jdk_x64_linux_hotspot_18.0.2.1_1.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
curl -LfsSo /tmp/openjdk.tar.gz ${BINARY_URL}; \
echo "${ESUM} */tmp/openjdk.tar.gz" | sha256sum -c -; \
mkdir -p /opt/jdk; \
cd /opt/jdk; \
tar -xf /tmp/openjdk.tar.gz --strip-components=1; \
rm -rf /tmp/openjdk.tar.gz;
RUN /opt/jdk/bin/jlink \
--verbose \
--module-path /opt/jdk/jmods \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress 0 \
--add-modules java.base,java.naming,java.xml,java.logging,java.net.http,java.sql,java.management,java.desktop,jdk.jfr,jdk.unsupported,jdk.management.jfr \
--output /opt/jre
FROM ubuntu:22.04
LABEL maintainer="Andrey Antukh <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
COPY --from=jre-build /opt/jre /opt/jre
ENV LANG='en_US.UTF-8' LC_ALL='en_US.UTF-8'
WORKDIR /root
RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
apt-get -qq update; \
apt-get -qqy --no-install-recommends install \
curl \
@@ -80,8 +23,34 @@ RUN set -ex; \
locale-gen; \
rm -rf /var/lib/apt/lists/*;
COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='37ceaf232a85cce46bcccfd71839854e8b14bf3160e7ef72a676b9cae45ee8af'; \
BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_linux_hotspot_18.0.1_10.tar.gz'; \
;; \
armhf|armv7l) \
ESUM='0ddec3c165ab0b662a57a845db3fdaeb840660b493f164696b03df76aadf61c8'; \
BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_arm_linux_hotspot_18.0.1_10.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='16b1d9d75f22c157af04a1fd9c664324c7f4b5163c022b382a2f2e8897c1b0a2'; \
BINARY_URL='https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
curl -LfsSo /tmp/openjdk.tar.gz ${BINARY_URL}; \
echo "${ESUM} */tmp/openjdk.tar.gz" | sha256sum -c -; \
mkdir -p /usr/lib/jvm/openjdk; \
cd /usr/lib/jvm/openjdk; \
tar -xf /tmp/openjdk.tar.gz --strip-components=1; \
rm -rf /tmp/openjdk.tar.gz;
USER penpot:penpot
ENV JAVA_HOME=/usr/lib/jvm/openjdk PATH="/usr/lib/jvm/openjdk/bin:$PATH"
ADD ./bundle-backend/ /opt/penpot/backend/
WORKDIR /opt/penpot/backend
CMD ["/bin/bash", "run.sh"]

View File

@@ -1,76 +1,70 @@
FROM ubuntu:22.04
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
ARG DEBIAN_FRONTEND=noninteractive
ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
NODE_VERSION=v18.12.1 \
DEBIAN_FRONTEND=noninteractive \
PATH=/opt/node/bin:$PATH
NODE_VERSION=v16.17.0
RUN set -ex; \
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
mkdir -p /etc/resolvconf/resolv.conf.d; \
echo "nameserver 127.0.0.11" > /etc/resolvconf/resolv.conf.d/tail; \
echo "nameserver 8.8.8.8" > /etc/resolvconf/resolv.conf.d/tail; \
apt-get -qq update; \
apt-get -qqy --no-install-recommends install \
curl \
tzdata \
locales \
ca-certificates \
fontconfig \
xz-utils \
; \
rm -rf /var/lib/apt/lists/*; \
apt-get -qqy --no-install-recommends install curl tzdata locales ca-certificates fontconfig xz-utils; \
echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \
locale-gen;
locale-gen; \
rm -rf /var/lib/apt/lists/*;
RUN set -ex; \
apt-get -qq update; \
apt-get -qqy install \
imagemagick \
ghostscript \
netpbm \
poppler-utils \
potrace \
gconf-service \
libasound2 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatomic1 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgcc1 \
libgconf-2-4 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcb-dri3-0 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxshmfence1 \
libxss1 \
libxtst6 \
fonts-liberation \
libnss3 \
libgbm1 \
imagemagick \
ghostscript \
netpbm \
poppler-utils \
potrace \
gconf-service \
libasound2 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatomic1 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgcc1 \
libgconf-2-4 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcb-dri3-0 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxshmfence1 \
libxss1 \
libxtst6 \
fonts-liberation \
libnss3 \
libgbm1 \
; \
rm -rf /var/lib/apt/lists/*;
ENV PATH="/usr/local/nodejs/bin:$PATH"
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
@@ -89,22 +83,19 @@ RUN set -eux; \
;; \
esac; \
curl -LfsSo /tmp/nodejs.tar.xz ${BINARY_URL}; \
mkdir -p /opt/node; \
cd /opt/node; \
mkdir -p /usr/local/nodejs; \
cd /usr/local/nodejs; \
tar -xf /tmp/nodejs.tar.xz --strip-components=1; \
chown -R root /opt/node; \
npm install -g yarn; \
rm -rf /tmp/nodejs.tar.xz; \
mkdir -p /opt/penpot; \
chown -R penpot:penpot /opt/penpot;
chown -R root /usr/local/nodejs; \
/usr/local/nodejs/bin/npm install -g yarn; \
rm -rf /tmp/nodejs.tar.xz;
ADD --chown=penpot:penpot ./bundle-exporter/ /opt/penpot/exporter
WORKDIR /opt/app
WORKDIR /opt/penpot/exporter
USER penpot:penpot
ADD ./bundle-exporter/ /opt/app/
RUN set -ex; \
yarn; \
yarn run playwright install chromium;
npx playwright install chromium;
CMD ["node", "app.js"]
CMD ["/usr/local/nodejs/bin/node", "app.js"]

96
docker/images/config.env Normal file
View File

@@ -0,0 +1,96 @@
## 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

Some files were not shown because too many files have changed in this diff Show More