mirror of
https://github.com/penpot/penpot.git
synced 2025-02-13 10:38:13 -05:00
Merge pull request #2346 from penpot/niwinz-hot-improvements
Improvements & Fixes (part 2)
This commit is contained in:
commit
35a72be4f2
283 changed files with 1547 additions and 1251 deletions
|
@ -2,8 +2,8 @@
|
|||
{promesa.core/let clojure.core/let
|
||||
promesa.core/->> clojure.core/->>
|
||||
promesa.core/-> clojure.core/->
|
||||
rumext.alpha/defc clojure.core/defn
|
||||
rumext.alpha/fnc clojure.core/fn
|
||||
rumext.v2/defc clojure.core/defn
|
||||
rumext.v2/fnc clojure.core/fn
|
||||
app.common.data/export clojure.core/def
|
||||
app.db/with-atomic clojure.core/with-open
|
||||
app.common.data.macros/get-in clojure.core/get-in
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
- Fix inconsistent message on deleting library when a library is linked from deleted files
|
||||
- Fix change multiple colors with SVG [Taiga #3889](https://tree.taiga.io/project/penpot/issue/3889)
|
||||
- Fix ungroup does not work for typographies [Taiga #4195](https://tree.taiga.io/project/penpot/issue/4195)
|
||||
- Fix inviting to non existing users can fail [Taiga #4108](https://tree.taiga.io/project/penpot/issue/4108)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
@ -68,6 +69,7 @@
|
|||
- Fix Terms and Privacy links overlapping [Taiga #4137](https://tree.taiga.io/project/penpot/issue/4137)
|
||||
- Fix Export bounding box mask [Taiga #950](https://tree.taiga.io/project/penpot/issue/950)
|
||||
- Fix delete layers in bulk [Taiga #4160](https://tree.taiga.io/project/penpot/issue/4160)
|
||||
- Fix Cannot take out an element from a group at layers panel by drag [Taiga #4209](https://tree.taiga.io/project/penpot/issue/4209)
|
||||
|
||||
|
||||
## 1.15.3-beta
|
||||
|
|
|
@ -6,26 +6,26 @@
|
|||
;; Logging
|
||||
org.zeromq/jeromq {:mvn/version "0.5.2"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.2-3"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.2-4"}
|
||||
org.clojure/data.fressian {:mvn/version "1.0.0"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.15.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.15.0"}
|
||||
io.prometheus/simpleclient_jetty {:mvn/version "0.15.0"
|
||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
||||
io.prometheus/simpleclient_jetty {:mvn/version "0.16.0"
|
||||
:exclusions [org.eclipse.jetty/jetty-server
|
||||
org.eclipse.jetty/jetty-servlet]}
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.15.0"}
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "6.1.8.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.2.0.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/yetti {:git/tag "v9.8" :git/sha "fbe1d7d"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.2.780"}
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.828"}
|
||||
metosin/reitit-core {:mvn/version "0.5.18"}
|
||||
org.postgresql/postgresql {:mvn/version "42.4.0"}
|
||||
org.postgresql/postgresql {:mvn/version "42.5.0"}
|
||||
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
|
||||
|
||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||
|
@ -42,14 +42,12 @@
|
|||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||
integrant/integrant {:mvn/version "0.8.0"}
|
||||
|
||||
io.sentry/sentry {:mvn/version "5.6.1"}
|
||||
|
||||
dawran6/emoji {:mvn/version "0.1.5"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.11.1"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.11.3"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.17.272"}}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.17.278"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
|
@ -65,8 +63,7 @@
|
|||
:extra-paths ["test" "dev"]}
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.8.2" :git/sha "ba1a2bf"}}
|
||||
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
|
|
|
@ -434,6 +434,10 @@
|
|||
(assoc :path "/#/auth/verify-token")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
(when (:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(when (fn? audit)
|
||||
(audit :cmd :submit
|
||||
:type "command"
|
||||
|
|
|
@ -198,11 +198,6 @@
|
|||
(s/def ::telemetry-with-taiga ::us/boolean)
|
||||
(s/def ::tenant ::us/string)
|
||||
|
||||
(s/def ::sentry-trace-sample-rate ::us/number)
|
||||
(s/def ::sentry-attach-stack-trace ::us/boolean)
|
||||
(s/def ::sentry-debug ::us/boolean)
|
||||
(s/def ::sentry-dsn ::us/string)
|
||||
|
||||
(s/def ::config
|
||||
(s/keys :opt-un [::secret-key
|
||||
::flags
|
||||
|
@ -276,17 +271,13 @@
|
|||
::public-uri
|
||||
::redis-uri
|
||||
::registration-domain-whitelist
|
||||
::rpc-rlimit-config
|
||||
|
||||
::semaphore-process-font
|
||||
::semaphore-process-image
|
||||
::semaphore-update-file
|
||||
::semaphore-auth
|
||||
|
||||
::rpc-rlimit-config
|
||||
::sentry-dsn
|
||||
::sentry-debug
|
||||
::sentry-attach-stack-trace
|
||||
::sentry-trace-sample-rate
|
||||
::smtp-default-from
|
||||
::smtp-default-reply-to
|
||||
::smtp-host
|
||||
|
@ -295,8 +286,10 @@
|
|||
::smtp-ssl
|
||||
::smtp-tls
|
||||
::smtp-username
|
||||
|
||||
::srepl-host
|
||||
::srepl-port
|
||||
|
||||
::assets-storage-backend
|
||||
::storage-assets-fs-directory
|
||||
::storage-assets-s3-bucket
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.db
|
||||
(:refer-clojure :exclude [get])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
|
@ -270,28 +271,55 @@
|
|||
(sql/delete table params opts)
|
||||
(assoc opts :return-keys true))))
|
||||
|
||||
(defn- is-deleted?
|
||||
(defn is-row-deleted?
|
||||
[{:keys [deleted-at]}]
|
||||
(and (dt/instant? deleted-at)
|
||||
(< (inst-ms deleted-at)
|
||||
(inst-ms (dt/now)))))
|
||||
|
||||
(defn get-by-params
|
||||
(defn get*
|
||||
"Internal function for retrieve a single row from database that
|
||||
matches a simple filters."
|
||||
([ds table params]
|
||||
(get-by-params ds table params nil))
|
||||
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
|
||||
(let [res (exec-one! ds (sql/select table params opts))]
|
||||
(when (and check-not-found (or (not res) (is-deleted? res)))
|
||||
(get* ds table params nil))
|
||||
([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}]
|
||||
(let [rows (exec! ds (sql/select table params opts))
|
||||
rows (cond->> rows
|
||||
check-deleted?
|
||||
(remove is-row-deleted?))]
|
||||
(first rows))))
|
||||
|
||||
(defn get
|
||||
([ds table params]
|
||||
(get ds table params nil))
|
||||
([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}]
|
||||
(let [row (get* ds table params opts)]
|
||||
(when (and (not row) check-deleted?)
|
||||
(ex/raise :type :not-found
|
||||
:table table
|
||||
:hint "database object not found"))
|
||||
res)))
|
||||
row)))
|
||||
|
||||
(defn get-by-params
|
||||
"DEPRECATED"
|
||||
([ds table params]
|
||||
(get-by-params ds table params nil))
|
||||
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
|
||||
(let [row (get* ds table params (assoc opts :check-deleted? check-not-found))]
|
||||
(when (and (not row) check-not-found)
|
||||
(ex/raise :type :not-found
|
||||
:table table
|
||||
:hint "database object not found"))
|
||||
row)))
|
||||
|
||||
(defn get-by-id
|
||||
([ds table id]
|
||||
(get-by-params ds table {:id id} nil))
|
||||
(get ds table {:id id} nil))
|
||||
([ds table id opts]
|
||||
(get-by-params ds table {:id id} opts)))
|
||||
(let [opts (cond-> opts
|
||||
(contains? opts :check-not-found)
|
||||
(assoc :check-deleted? (:check-not-found opts)))]
|
||||
(get ds table {:id id} opts))))
|
||||
|
||||
(defn query
|
||||
([ds table params]
|
||||
|
|
|
@ -56,7 +56,6 @@
|
|||
type (resolve-recipient-type type)]
|
||||
(.addRecipients mmsg type address)
|
||||
mmsg)))
|
||||
|
||||
(defn- assign-recipients
|
||||
[mmsg {:keys [to cc bcc] :as params}]
|
||||
(cond-> mmsg
|
||||
|
@ -139,6 +138,7 @@
|
|||
(Properties.)
|
||||
{"mail.user" username
|
||||
"mail.host" host
|
||||
"mail.debug" (contains? cf/flags :smtp-debug)
|
||||
"mail.from" default-from
|
||||
"mail.smtp.auth" (boolean username)
|
||||
"mail.smtp.starttls.enable" tls
|
||||
|
@ -150,17 +150,14 @@
|
|||
"mail.smtp.connectiontimeout" timeout}))
|
||||
|
||||
(defn- create-smtp-session
|
||||
[{:keys [debug] :or {debug false} :as opts}]
|
||||
(let [props (opts->props opts)
|
||||
session (Session/getInstance props)]
|
||||
(.setDebug session debug)
|
||||
session))
|
||||
[opts]
|
||||
(let [props (opts->props opts)]
|
||||
(Session/getInstance props)))
|
||||
|
||||
(defn- create-smtp-message
|
||||
^MimeMessage
|
||||
[cfg params]
|
||||
(let [session (create-smtp-session cfg)
|
||||
mmsg (MimeMessage. ^Session session)]
|
||||
[cfg session params]
|
||||
(let [mmsg (MimeMessage. ^Session session)]
|
||||
(assign-recipients mmsg params)
|
||||
(assign-from mmsg cfg params)
|
||||
(assign-reply-to mmsg cfg params)
|
||||
|
@ -304,9 +301,16 @@
|
|||
[_ cfg]
|
||||
(fn [params]
|
||||
(when (contains? cf/flags :smtp)
|
||||
(Transport/send (create-smtp-message cfg params)
|
||||
(:username cfg)
|
||||
(:password cfg)))
|
||||
(let [session (create-smtp-session cfg)]
|
||||
(with-open [transport (.getTransport session (if (:ssl cfg) "smtps" "smtp"))]
|
||||
(.connect ^Transport transport
|
||||
^String (:username cfg)
|
||||
^String (:password cfg))
|
||||
|
||||
(let [^MimeMessage message (create-smtp-message cfg session params)]
|
||||
(.sendMessage ^Transport transport
|
||||
^MimeMessage message
|
||||
(.getAllRecipients message))))))
|
||||
|
||||
(when (or (contains? cf/flags :log-emails)
|
||||
(not (contains? cf/flags :smtp)))
|
||||
|
|
|
@ -52,13 +52,6 @@
|
|||
(let [mdata (meta obj)
|
||||
backend (sto/resolve-backend storage (:backend obj))]
|
||||
(case (:type backend)
|
||||
:db
|
||||
(p/let [body (sto/get-object-bytes storage obj)]
|
||||
(yrs/response :status 200
|
||||
:body body
|
||||
:headers {"content-type" (:content-type mdata)
|
||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}))
|
||||
|
||||
:s3
|
||||
(p/let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
|
||||
(yrs/response :status 307
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:refer-clojure :exclude [error-handler])
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
|
@ -213,7 +214,7 @@
|
|||
|
||||
(render-template [report]
|
||||
(let [context (dissoc report
|
||||
:trace :cause :params :data :spec-problems
|
||||
:trace :cause :params :data :spec-problems :message
|
||||
:spec-explain :spec-value :error :explain :hint)
|
||||
params {:context (pp/pprint-str context :width 200)
|
||||
:hint (:hint report)
|
||||
|
@ -341,8 +342,13 @@
|
|||
"Mainly a task that performs a health check."
|
||||
[{:keys [pool]} _]
|
||||
(db/with-atomic [conn pool]
|
||||
(db/exec-one! conn ["select count(*) as count from server_prop;"])
|
||||
(yrs/response 200 "OK")))
|
||||
(try
|
||||
(db/exec-one! conn ["select count(*) as count from server_prop;"])
|
||||
(yrs/response 200 "OK")
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unable to execute query on health handler"
|
||||
:cause cause)
|
||||
(yrs/response 503 "KO")))))
|
||||
|
||||
(defn changelog-handler
|
||||
[_ _]
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
(defn parse-event
|
||||
[event]
|
||||
(-> (parse-event-data event)
|
||||
(assoc :hint (or (:hint event) (:message event)))
|
||||
(assoc :tenant (cf/get :tenant))
|
||||
(assoc :host (cf/get :host))
|
||||
(assoc :public-uri (cf/get :public-uri))
|
||||
|
|
|
@ -1,170 +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.loggers.sentry
|
||||
"A mattermost integration for error reporting."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.async :as aa]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
io.sentry.Scope
|
||||
io.sentry.IHub
|
||||
io.sentry.Hub
|
||||
io.sentry.NoOpHub
|
||||
io.sentry.protocol.User
|
||||
io.sentry.SentryOptions
|
||||
io.sentry.SentryLevel
|
||||
io.sentry.ScopeCallback))
|
||||
|
||||
(defonce enabled (atom true))
|
||||
|
||||
(defn- parse-context
|
||||
[event]
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
(cond
|
||||
(= k :id) (assoc acc k (uuid/uuid v))
|
||||
(= k :profile-id) (assoc acc k (uuid/uuid v))
|
||||
(str/blank? v) acc
|
||||
:else (assoc acc k v)))
|
||||
{}
|
||||
(:context event)))
|
||||
|
||||
(defn- parse-event
|
||||
[event]
|
||||
(assoc event :context (parse-context event)))
|
||||
|
||||
(defn- build-sentry-options
|
||||
[cfg]
|
||||
(let [version (:base cf/version)]
|
||||
(doto (SentryOptions.)
|
||||
(.setDebug (:debug cfg false))
|
||||
(.setTracesSampleRate (:traces-sample-rate cfg 1.0))
|
||||
(.setDsn (:dsn cfg))
|
||||
(.setServerName (cf/get :host))
|
||||
(.setEnvironment (cf/get :tenant))
|
||||
(.setAttachServerName true)
|
||||
(.setAttachStacktrace (:attach-stack-trace cfg false))
|
||||
(.setRelease (str "backend@" (if (= version "0.0.0") "develop" version))))))
|
||||
|
||||
(defn handle-event
|
||||
[^IHub shub event]
|
||||
(letfn [(set-user! [^Scope scope {:keys [context] :as event}]
|
||||
(let [user (User.)]
|
||||
(.setIpAddress ^User user ^String (:ip-addr context))
|
||||
(when-let [pid (:profile-id context)]
|
||||
(.setId ^User user ^String (str pid)))
|
||||
(.setUser scope ^User user)))
|
||||
|
||||
(set-level! [^Scope scope]
|
||||
(.setLevel scope SentryLevel/ERROR))
|
||||
|
||||
(set-context! [^Scope scope {:keys [context] :as event}]
|
||||
(let [uri (str (cf/get :public-uri) "/dbg/error-by-id/" (:id context))]
|
||||
(.setContexts scope "detailed_error_uri" ^String uri))
|
||||
(when-let [vers (:frontend-version event)]
|
||||
(.setContexts scope "frontend_version" ^String vers))
|
||||
(when-let [puri (:public-uri event)]
|
||||
(.setContexts scope "public_uri" ^String (str puri)))
|
||||
(when-let [uagent (:user-agent context)]
|
||||
(.setContexts scope "user_agent" ^String uagent))
|
||||
(when-let [tenant (:tenant event)]
|
||||
(.setTag scope "tenant" ^String tenant))
|
||||
(when-let [type (:error-type context)]
|
||||
(.setTag scope "error_type" ^String (str type)))
|
||||
(when-let [code (:error-code context)]
|
||||
(.setTag scope "error_code" ^String (str code)))
|
||||
)
|
||||
|
||||
(capture [^Scope scope {:keys [context error] :as event}]
|
||||
(let [msg (str (:message error) "\n\n"
|
||||
|
||||
"======================================================\n"
|
||||
"=================== Params ===========================\n"
|
||||
"======================================================\n"
|
||||
|
||||
(:params context) "\n"
|
||||
|
||||
(when (:explain context)
|
||||
(str "======================================================\n"
|
||||
"=================== Explain ==========================\n"
|
||||
"======================================================\n"
|
||||
(:explain context) "\n"))
|
||||
|
||||
(when (:data context)
|
||||
(str "======================================================\n"
|
||||
"=================== Error Data =======================\n"
|
||||
"======================================================\n"
|
||||
(:data context) "\n"))
|
||||
|
||||
(str "======================================================\n"
|
||||
"=================== Stack Trace ======================\n"
|
||||
"======================================================\n"
|
||||
(:trace error))
|
||||
|
||||
"\n")]
|
||||
(set-user! scope event)
|
||||
(set-level! scope)
|
||||
(set-context! scope event)
|
||||
(.captureMessage ^IHub shub msg)
|
||||
))
|
||||
]
|
||||
(when @enabled
|
||||
(.withScope ^IHub shub (reify ScopeCallback
|
||||
(run [_ scope]
|
||||
(->> event
|
||||
(parse-event)
|
||||
(capture scope))))))
|
||||
|
||||
))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Error Listener
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::receiver any?)
|
||||
(s/def ::dsn ::cf/sentry-dsn)
|
||||
(s/def ::trace-sample-rate ::cf/sentry-trace-sample-rate)
|
||||
(s/def ::attach-stack-trace ::cf/sentry-attach-stack-trace)
|
||||
(s/def ::debug ::cf/sentry-debug)
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]
|
||||
:opt-un [::dsn ::trace-sample-rate ::attach-stack-trace]))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ {:keys [receiver dsn executor] :as cfg}]
|
||||
(l/info :msg "initializing sentry reporter" :dsn dsn)
|
||||
(let [opts (build-sentry-options cfg)
|
||||
shub (if dsn
|
||||
(Hub. ^SentryOptions opts)
|
||||
(NoOpHub/getInstance))
|
||||
output (a/chan (a/sliding-buffer 128)
|
||||
(filter #(= (:level %) "error")))]
|
||||
(receiver :sub output)
|
||||
(a/go-loop []
|
||||
(let [event (a/<! output)]
|
||||
(if (nil? event)
|
||||
(do
|
||||
(l/info :msg "stoping error reporting loop")
|
||||
(.close ^IHub shub))
|
||||
(do
|
||||
(a/<! (aa/with-thread executor (handle-event shub event)))
|
||||
(recur)))))
|
||||
output))
|
||||
|
||||
(defmethod ig/halt-key! ::reporter
|
||||
[_ output]
|
||||
(when output
|
||||
(a/close! output)))
|
|
@ -244,6 +244,9 @@
|
|||
|
||||
{:name "0078-mod-file-media-object-table-drop-cascade"
|
||||
:fn (mg/resource "app/migrations/sql/0078-mod-file-media-object-table-drop-cascade.sql")}
|
||||
|
||||
{:name "0079-mod-profile-table"
|
||||
:fn (mg/resource "app/migrations/sql/0079-mod-profile-table.sql")}
|
||||
])
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE profile
|
||||
ADD COLUMN is_blocked boolean DEFAULT false;
|
|
@ -31,9 +31,10 @@
|
|||
|
||||
(defn- handle-response-transformation
|
||||
[response request mdata]
|
||||
(if-let [transform-fn (:transform-response mdata)]
|
||||
(p/do (transform-fn request response))
|
||||
(p/resolved response)))
|
||||
(let [response (if (sv/wrapped? response) @response response)]
|
||||
(if-let [transform-fn (:transform-response mdata)]
|
||||
(p/do (transform-fn request response))
|
||||
(p/resolved response))))
|
||||
|
||||
(defn- handle-before-comple-hook
|
||||
[response mdata]
|
||||
|
@ -222,6 +223,7 @@
|
|||
(->> (sv/scan-ns 'app.rpc.commands.binfile
|
||||
'app.rpc.commands.comments
|
||||
'app.rpc.commands.management
|
||||
'app.rpc.commands.verify-token
|
||||
'app.rpc.commands.auth
|
||||
'app.rpc.commands.ldap
|
||||
'app.rpc.commands.demo
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.rpc.commands.auth
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
|
@ -96,15 +97,19 @@
|
|||
(:valid (verify-password password (:password profile))))
|
||||
|
||||
(validate-profile [profile]
|
||||
(when-not (:is-active profile)
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
(when-not profile
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
(when-not (:is-active profile)
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
(when (:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
(when-not (check-password profile password)
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
|
||||
profile)]
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
|
@ -184,8 +189,9 @@
|
|||
|
||||
;; ---- COMMAND: Prepare Register
|
||||
|
||||
(defn prepare-register
|
||||
[{:keys [pool sprops] :as cfg} params]
|
||||
(defn validate-register-attempt!
|
||||
[{:keys [pool sprops]} params]
|
||||
|
||||
(when-not (contains? cf/flags :registration)
|
||||
(if-not (contains? params :invitation-token)
|
||||
(ex/raise :type :restriction
|
||||
|
@ -208,20 +214,50 @@
|
|||
:code :email-has-permanent-bounces
|
||||
:hint "looks like the email has one or many bounces reported"))
|
||||
|
||||
(check-profile-existence! pool params)
|
||||
|
||||
;; Perform a basic validation of email & password
|
||||
(when (= (str/lower (:email params))
|
||||
(str/lower (:password params)))
|
||||
(ex/raise :type :validation
|
||||
:code :email-as-password
|
||||
:hint "you can't use your email as password"))
|
||||
:hint "you can't use your email as password")))
|
||||
|
||||
(let [params {:email (:email params)
|
||||
:password (:password params)
|
||||
:invitation-token (:invitation-token params)
|
||||
:backend "penpot"
|
||||
:iss :prepared-register
|
||||
:exp (dt/in-future "48h")}
|
||||
(def register-retry-threshold
|
||||
(dt/duration "15m"))
|
||||
|
||||
(defn- elapsed-register-retry-threshold?
|
||||
[profile]
|
||||
(let [elapsed (dt/diff (:modified-at profile) (dt/now))]
|
||||
(pos? (compare elapsed register-retry-threshold))))
|
||||
|
||||
(defn prepare-register
|
||||
[{:keys [pool sprops] :as cfg} params]
|
||||
|
||||
(validate-register-attempt! cfg params)
|
||||
|
||||
(let [profile (when-let [profile (profile/retrieve-profile-data-by-email pool (:email params))]
|
||||
(cond
|
||||
(:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked)
|
||||
|
||||
(and (not (:is-active profile))
|
||||
(elapsed-register-retry-threshold? profile))
|
||||
profile
|
||||
|
||||
:else
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists
|
||||
:hint "profile already exists")))
|
||||
|
||||
params {:email (:email params)
|
||||
:password (:password params)
|
||||
:invitation-token (:invitation-token params)
|
||||
:backend "penpot"
|
||||
:iss :prepared-register
|
||||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 7})}
|
||||
|
||||
params (d/without-nils params)
|
||||
|
||||
token (tokens/generate sprops params)]
|
||||
(with-meta {:token token}
|
||||
|
@ -240,11 +276,10 @@
|
|||
;; ---- COMMAND: Register Profile
|
||||
|
||||
(defn create-profile
|
||||
"Create the profile entry on the database with limited input filling
|
||||
all the other fields with defaults."
|
||||
"Create the profile entry on the database with limited set of input
|
||||
attrs (all the other attrs are filled with default values)."
|
||||
[conn params]
|
||||
(let [id (or (:id params) (uuid/next))
|
||||
|
||||
props (-> (audit/extract-utm-params params)
|
||||
(merge (:props params))
|
||||
(merge {:viewed-tutorial? false
|
||||
|
@ -320,62 +355,76 @@
|
|||
|
||||
(defn register-profile
|
||||
[{:keys [conn sprops session] :as cfg} {:keys [token] :as params}]
|
||||
(let [claims (tokens/verify sprops {:token token :iss :prepared-register})
|
||||
params (merge params claims)]
|
||||
(check-profile-existence! conn params)
|
||||
(let [is-active (or (:is-active params)
|
||||
(not (contains? cf/flags :email-verification))
|
||||
(let [claims (tokens/verify sprops {:token token :iss :prepared-register})
|
||||
params (merge params claims)
|
||||
|
||||
;; DEPRECATED: v1.15
|
||||
(contains? cf/flags :insecure-register))
|
||||
is-active (or (:is-active params)
|
||||
(not (contains? cf/flags :email-verification))
|
||||
|
||||
profile (->> (assoc params :is-active is-active)
|
||||
;; DEPRECATED: v1.15
|
||||
(contains? cf/flags :insecure-register))
|
||||
|
||||
profile (if-let [profile-id (:profile-id claims)]
|
||||
(profile/retrieve-profile conn profile-id)
|
||||
(->> (assoc params :is-active is-active)
|
||||
(create-profile conn)
|
||||
(create-profile-relations conn)
|
||||
(profile/decode-profile-row))
|
||||
(profile/decode-profile-row)))
|
||||
audit-fn (:audit cfg)
|
||||
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens/verify sprops {:token token :iss :team-invitation}))]
|
||||
(cond
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
;; regenerate token and send back to the user a new invitation
|
||||
;; token (and mark current session as logged). This happens
|
||||
;; only if the invitation email matches with the register
|
||||
;; email.
|
||||
(and (some? invitation) (= (:email profile) (:member-email invitation)))
|
||||
(let [claims (assoc invitation :member-id (:id profile))
|
||||
token (tokens/generate sprops claims)
|
||||
resp {:invitation-token token}]
|
||||
(with-meta resp
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens/verify sprops {:token token :iss :team-invitation}))]
|
||||
|
||||
;; If auth backend is different from "penpot" means user is
|
||||
;; registering using third party auth mechanism; in this case
|
||||
;; we need to mark this session as logged.
|
||||
(not= "penpot" (:auth-backend profile))
|
||||
(with-meta (profile/strip-private-attrs profile)
|
||||
;; If profile is filled in claims, means it tries to register
|
||||
;; again, so we proceed to update the modified-at attr
|
||||
;; accordingly.
|
||||
(when-let [id (:profile-id claims)]
|
||||
(db/update! conn :profile {:modified-at (dt/now)} {:id id})
|
||||
(audit-fn :cmd :submit
|
||||
:type "fact"
|
||||
:name "register-profile-retry"
|
||||
:profile-id id))
|
||||
|
||||
(cond
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
;; regenerate token and send back to the user a new invitation
|
||||
;; token (and mark current session as logged). This happens
|
||||
;; only if the invitation email matches with the register
|
||||
;; email.
|
||||
(and (some? invitation) (= (:email profile) (:member-email invitation)))
|
||||
(let [claims (assoc invitation :member-id (:id profile))
|
||||
token (tokens/generate sprops claims)
|
||||
resp {:invitation-token token}]
|
||||
(with-meta resp
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})
|
||||
::audit/profile-id (:id profile)}))
|
||||
|
||||
;; If the `:enable-insecure-register` flag is set, we proceed
|
||||
;; to sign in the user directly, without email verification.
|
||||
(true? is-active)
|
||||
(with-meta (profile/strip-private-attrs profile)
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})
|
||||
;; If auth backend is different from "penpot" means user is
|
||||
;; registering using third party auth mechanism; in this case
|
||||
;; we need to mark this session as logged.
|
||||
(not= "penpot" (:auth-backend profile))
|
||||
(with-meta (profile/strip-private-attrs profile)
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})
|
||||
|
||||
;; In all other cases, send a verification email.
|
||||
:else
|
||||
(do
|
||||
(send-email-verification! conn sprops profile)
|
||||
(with-meta profile
|
||||
{::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))))))
|
||||
;; If the `:enable-insecure-register` flag is set, we proceed
|
||||
;; to sign in the user directly, without email verification.
|
||||
(true? is-active)
|
||||
(with-meta (profile/strip-private-attrs profile)
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})
|
||||
|
||||
;; In all other cases, send a verification email.
|
||||
:else
|
||||
(do
|
||||
(send-email-verification! conn sprops profile)
|
||||
(with-meta profile
|
||||
{::audit/replace-props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})))))
|
||||
|
||||
(s/def ::register-profile
|
||||
(s/keys :req-un [::token ::fullname]))
|
||||
|
|
|
@ -46,6 +46,11 @@
|
|||
:code :wrong-credentials))
|
||||
|
||||
(let [profile (login-or-register cfg info)]
|
||||
|
||||
(when (:is-blocked profile)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
(if-let [token (:invitation-token params)]
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
|
|
193
backend/src/app/rpc/commands/verify_token.clj
Normal file
193
backend/src/app/rpc/commands/verify_token.clj
Normal file
|
@ -0,0 +1,193 @@
|
|||
;; 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.verify-token
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[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]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::iss keyword?)
|
||||
(s/def ::exp ::us/inst)
|
||||
|
||||
(defmulti process-token (fn [_ _ claims] (:iss claims)))
|
||||
|
||||
(s/def ::verify-token
|
||||
(s/keys :req-un [::token]
|
||||
:opt-un [::profile-id]))
|
||||
|
||||
(sv/defmethod ::verify-token
|
||||
{:auth false
|
||||
::doc/added "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))))
|
||||
|
||||
(defmethod process-token :change-email
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
|
||||
(when (profile/retrieve-profile-data-by-email conn email)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:email email}
|
||||
{:id profile-id})
|
||||
|
||||
(with-meta claims
|
||||
{::audit/name "update-profile-email"
|
||||
::audit/props {:email email}
|
||||
::audit/profile-id profile-id}))
|
||||
|
||||
(defmethod process-token :verify-email
|
||||
[{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/retrieve-profile conn profile-id)
|
||||
claims (assoc claims :profile profile)]
|
||||
|
||||
(when-not (:is-active profile)
|
||||
(when (not= (:email profile)
|
||||
(:email claims))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id profile)}))
|
||||
|
||||
(with-meta claims
|
||||
{:transform-response ((:create session) profile-id)
|
||||
::audit/name "verify-profile-email"
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})))
|
||||
|
||||
(defmethod process-token :auth
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/retrieve-profile conn profile-id)]
|
||||
(assoc claims :profile profile)))
|
||||
|
||||
;; --- Team Invitation
|
||||
|
||||
(defn- accept-invitation
|
||||
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
|
||||
(let [;; Update the role if there is an invitation
|
||||
role (or (some-> invitation :role keyword) role)
|
||||
params (merge
|
||||
{:team-id team-id
|
||||
:profile-id (:id member)}
|
||||
(teams/role->params role))]
|
||||
|
||||
;; Do not allow blocked users accept invitations.
|
||||
(when (:is-blocked member)
|
||||
(ex/raise :type :restriction
|
||||
:code :profile-blocked))
|
||||
|
||||
;; 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)}))
|
||||
|
||||
;; Delete the invitation
|
||||
(db/delete! conn :team-invitation
|
||||
{:team-id team-id :email-to member-email})
|
||||
|
||||
(assoc member :is-active true)))
|
||||
|
||||
(s/def ::spec.team-invitation/profile-id ::us/uuid)
|
||||
(s/def ::spec.team-invitation/role ::us/keyword)
|
||||
(s/def ::spec.team-invitation/team-id ::us/uuid)
|
||||
(s/def ::spec.team-invitation/member-email ::us/email)
|
||||
(s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid))
|
||||
|
||||
(s/def ::team-invitation-claims
|
||||
(s/keys :req-un [::iss ::exp
|
||||
::spec.team-invitation/profile-id
|
||||
::spec.team-invitation/role
|
||||
::spec.team-invitation/team-id
|
||||
::spec.team-invitation/member-email]
|
||||
:opt-un [::spec.team-invitation/member-id]))
|
||||
|
||||
(defmethod process-token :team-invitation
|
||||
[{:keys [conn session] :as cfg} {:keys [profile-id token]}
|
||||
{:keys [member-id team-id member-email] :as claims}]
|
||||
|
||||
(us/assert ::team-invitation-claims claims)
|
||||
|
||||
(let [invitation (db/get* conn :team-invitation
|
||||
{:team-id team-id :email-to member-email})
|
||||
profile (db/get* conn :profile
|
||||
{:id profile-id}
|
||||
{:columns [:id :email]})]
|
||||
(when (nil? invitation)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token
|
||||
:hint "no invitation associated with the token"))
|
||||
|
||||
(if (some? profile)
|
||||
(if (or (= member-id profile-id)
|
||||
(= member-email (:email profile)))
|
||||
;; if we have logged-in user and it matches the invitation we
|
||||
;; proceed with accepting the invitation and joining the
|
||||
;; current profile to the invited team.
|
||||
(let [profile (accept-invitation cfg claims invitation profile)]
|
||||
(with-meta
|
||||
(assoc claims :state :created)
|
||||
{::audit/name "accept-team-invitation"
|
||||
::audit/props (merge
|
||||
(audit/profile->props profile)
|
||||
{:team-id (:team-id claims)
|
||||
:role (:role claims)})
|
||||
::audit/profile-id profile-id}))
|
||||
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token
|
||||
:hint "logged-in user does not matches the invitation"))
|
||||
|
||||
;; If we have not logged-in user, we try find the invited
|
||||
;; profile by member-id or member-email props of the invitation
|
||||
;; token; If profile is found, we accept the invitation and
|
||||
;; leave the user logged-in.
|
||||
(if-let [member (db/get* conn :profile
|
||||
(if member-id
|
||||
{:id member-id}
|
||||
{:email member-email})
|
||||
{:columns [:id :email]})]
|
||||
(let [profile (accept-invitation cfg claims invitation member)]
|
||||
(with-meta
|
||||
(assoc claims :state :created)
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/name "accept-team-invitation"
|
||||
::audit/props (merge
|
||||
(audit/profile->props profile)
|
||||
{:team-id (:team-id claims)
|
||||
:role (:role claims)})
|
||||
::audit/profile-id member-id}))
|
||||
|
||||
{:invitation-token token
|
||||
:iss :team-invitation
|
||||
:redirect-to :auth-register
|
||||
:state :pending}))))
|
||||
|
||||
;; --- Default
|
||||
|
||||
(defmethod process-token :default
|
||||
[_ _ _]
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
|
@ -565,6 +565,8 @@
|
|||
(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 thumbnals."
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id revn data props]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
|
|
|
@ -252,6 +252,7 @@
|
|||
|
||||
;; --- MUTATION: Delete Profile
|
||||
|
||||
(declare get-owned-teams-with-participants)
|
||||
(declare check-can-delete-profile!)
|
||||
(declare mark-profile-as-deleted!)
|
||||
|
||||
|
@ -261,14 +262,29 @@
|
|||
(sv/defmethod ::delete-profile
|
||||
[{:keys [pool session] :as cfg} {:keys [profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-can-delete-profile! conn profile-id)
|
||||
(let [teams (get-owned-teams-with-participants conn profile-id)
|
||||
deleted-at (dt/now)]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:deleted-at (dt/now)}
|
||||
{:id profile-id})
|
||||
;; If we found owned teams with participants, we don't allow
|
||||
;; delete profile until the user properly transfer ownership or
|
||||
;; explicitly removes all participants from the team
|
||||
(when (some pos? (map :participants teams))
|
||||
(ex/raise :type :validation
|
||||
:code :owner-teams-with-people
|
||||
:hint "The user need to transfer ownership of owned teams."
|
||||
:context {:teams (mapv :id teams)}))
|
||||
|
||||
(with-meta {}
|
||||
{:transform-response (:delete session)})))
|
||||
(doseq [{:keys [id]} teams]
|
||||
(db/update! conn :team
|
||||
{:deleted-at deleted-at}
|
||||
{:id id}))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:deleted-at deleted-at}
|
||||
{:id profile-id})
|
||||
|
||||
(with-meta {}
|
||||
{:transform-response (:delete session)}))))
|
||||
|
||||
(def sql:owned-teams
|
||||
"with owner_teams as (
|
||||
|
@ -277,23 +293,16 @@
|
|||
where tpr.is_owner is true
|
||||
and tpr.profile_id = ?
|
||||
)
|
||||
select tpr.team_id,
|
||||
count(tpr.profile_id) as num_profiles
|
||||
select tpr.team_id as id,
|
||||
count(tpr.profile_id) - 1 as participants
|
||||
from team_profile_rel as tpr
|
||||
where tpr.team_id in (select id from owner_teams)
|
||||
and tpr.profile_id != ?
|
||||
group by 1")
|
||||
|
||||
(defn- check-can-delete-profile!
|
||||
(defn- get-owned-teams-with-participants
|
||||
[conn profile-id]
|
||||
(let [rows (db/exec! conn [sql:owned-teams profile-id])]
|
||||
;; If we found owned teams with more than one profile we don't
|
||||
;; allow delete profile until the user properly transfer ownership
|
||||
;; or explicitly removes all participants from the team.
|
||||
(when (some #(> (:num-profiles %) 1) rows)
|
||||
(ex/raise :type :validation
|
||||
:code :owner-teams-with-people
|
||||
:hint "The user need to transfer ownership of owned teams."
|
||||
:context {:teams (mapv :team-id rows)}))))
|
||||
(db/exec! conn [sql:owned-teams profile-id profile-id]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; DEPRECATED METHODS (TO BE REMOVED ON 1.16.x)
|
||||
|
|
|
@ -376,18 +376,17 @@
|
|||
:code :profile-is-muted
|
||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||
|
||||
(doseq [email emails]
|
||||
(create-team-invitation
|
||||
(assoc cfg
|
||||
:email email
|
||||
:conn conn
|
||||
:team team
|
||||
:profile profile
|
||||
:role role))
|
||||
)
|
||||
|
||||
(with-meta {}
|
||||
{::audit/props {:invitations (count emails)}}))))
|
||||
(let [invitations (->> emails
|
||||
(map (fn [email]
|
||||
(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)
|
||||
|
@ -399,6 +398,7 @@
|
|||
[{: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
|
||||
|
@ -412,9 +412,6 @@
|
|||
:profile-id (:id profile)
|
||||
:exp (dt/in-future {:days 30})})]
|
||||
|
||||
(when (contains? cf/flags :log-invitation-tokens)
|
||||
(l/trace :hint "invitation token" :token itoken))
|
||||
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
:code :member-is-muted
|
||||
|
@ -428,6 +425,9 @@
|
|||
: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.
|
||||
|
@ -448,10 +448,7 @@
|
|||
(when-not (:is-active member)
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id member)}))
|
||||
|
||||
(assoc member :is-active true))
|
||||
|
||||
{:id (:id member)})))
|
||||
(do
|
||||
(db/exec-one! conn [sql:upsert-team-invitation
|
||||
(:id team) (str/lower email) (name role)
|
||||
|
@ -463,7 +460,9 @@
|
|||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:token itoken
|
||||
:extra-data ptoken})))))
|
||||
:extra-data ptoken})))
|
||||
|
||||
itoken))
|
||||
|
||||
;; --- Mutation: Create Team & Invite Members
|
||||
|
||||
|
|
|
@ -6,170 +6,23 @@
|
|||
|
||||
(ns app.rpc.mutations.verify-token
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc.mutations.teams :as teams]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.rpc.commands.verify-token :refer [process-token]]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.tokens :as tokens]
|
||||
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defmulti process-token (fn [_ _ claims] (:iss claims)))
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::verify-token
|
||||
(s/keys :req-un [::token]
|
||||
:opt-un [::profile-id]))
|
||||
|
||||
(sv/defmethod ::verify-token {:auth false}
|
||||
(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))))
|
||||
|
||||
(defmethod process-token :change-email
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}]
|
||||
(when (profile/retrieve-profile-data-by-email conn email)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:email email}
|
||||
{:id profile-id})
|
||||
|
||||
(with-meta claims
|
||||
{::audit/name "update-profile-email"
|
||||
::audit/props {:email email}
|
||||
::audit/profile-id profile-id}))
|
||||
|
||||
(defmethod process-token :verify-email
|
||||
[{:keys [conn session] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/retrieve-profile conn profile-id)
|
||||
claims (assoc claims :profile profile)]
|
||||
|
||||
(when-not (:is-active profile)
|
||||
(when (not= (:email profile)
|
||||
(:email claims))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id profile)}))
|
||||
|
||||
(with-meta claims
|
||||
{:transform-response ((:create session) profile-id)
|
||||
::audit/name "verify-profile-email"
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})))
|
||||
|
||||
(defmethod process-token :auth
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/retrieve-profile conn profile-id)]
|
||||
(assoc claims :profile profile)))
|
||||
|
||||
|
||||
;; --- Team Invitation
|
||||
|
||||
(s/def ::iss keyword?)
|
||||
(s/def ::exp ::us/inst)
|
||||
|
||||
(s/def ::spec.team-invitation/profile-id ::us/uuid)
|
||||
(s/def ::spec.team-invitation/role ::us/keyword)
|
||||
(s/def ::spec.team-invitation/team-id ::us/uuid)
|
||||
(s/def ::spec.team-invitation/member-email ::us/email)
|
||||
(s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid))
|
||||
|
||||
(s/def ::team-invitation-claims
|
||||
(s/keys :req-un [::iss ::exp
|
||||
::spec.team-invitation/profile-id
|
||||
::spec.team-invitation/role
|
||||
::spec.team-invitation/team-id
|
||||
::spec.team-invitation/member-email]
|
||||
:opt-un [::spec.team-invitation/member-id]))
|
||||
|
||||
(defn- accept-invitation
|
||||
[{:keys [conn] :as cfg} {:keys [member-id team-id role member-email] :as claims}]
|
||||
(let [
|
||||
member (profile/retrieve-profile conn member-id)
|
||||
invitation (db/get-by-params conn :team-invitation
|
||||
{:team-id team-id :email-to (str/lower member-email)}
|
||||
{:check-not-found false})
|
||||
;; Update the role if there is an invitation
|
||||
role (or (some-> invitation :role keyword) role)
|
||||
params (merge {:team-id team-id
|
||||
:profile-id member-id}
|
||||
(teams/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 member-id}))
|
||||
(assoc member :is-active true)
|
||||
|
||||
;; Delete the invitation
|
||||
(db/delete! conn :team-invitation
|
||||
{:team-id team-id :email-to (str/lower member-email)})))
|
||||
|
||||
|
||||
(defmethod process-token :team-invitation
|
||||
[cfg {:keys [profile-id token]} {:keys [member-id] :as claims}]
|
||||
(us/assert ::team-invitation-claims claims)
|
||||
(let [conn (:conn cfg)
|
||||
team-id (:team-id claims)
|
||||
member-email (:member-email claims)
|
||||
invitation (db/get-by-params conn :team-invitation
|
||||
{:team-id team-id :email-to (str/lower member-email)}
|
||||
{:check-not-found false})]
|
||||
(when (nil? invitation)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token)))
|
||||
|
||||
(cond
|
||||
;; This happens when token is filled with member-id and current
|
||||
;; user is already logged in with exactly invited account.
|
||||
(and (uuid? profile-id) (uuid? member-id) (= member-id profile-id))
|
||||
(let [profile (accept-invitation cfg claims)]
|
||||
(with-meta
|
||||
(assoc claims :state :created)
|
||||
{::audit/name "accept-team-invitation"
|
||||
::audit/props (merge
|
||||
(audit/profile->props profile)
|
||||
{:team-id (:team-id claims)
|
||||
:role (:role claims)})
|
||||
::audit/profile-id member-id}))
|
||||
|
||||
;; This case means that invitation token does not match with
|
||||
;; registred user, so we need to indicate to frontend to redirect
|
||||
;; it to register page.
|
||||
(nil? member-id)
|
||||
{:invitation-token token
|
||||
:iss :team-invitation
|
||||
:redirect-to :auth-register
|
||||
:state :pending}
|
||||
|
||||
;; In all other cases, just tell to fontend to redirect the user
|
||||
;; to the login page.
|
||||
:else
|
||||
{:invitation-token token
|
||||
:iss :team-invitation
|
||||
:redirect-to :auth-login
|
||||
:state :pending}))
|
||||
|
||||
;; --- Default
|
||||
|
||||
(defmethod process-token :default
|
||||
[_ _ _]
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
||||
|
|
|
@ -62,9 +62,9 @@
|
|||
(cmd.auth/send-email-verification! pool sprops profile)
|
||||
:email-sent))
|
||||
|
||||
(defn update-profile
|
||||
(defn update-profile!
|
||||
"Update a limited set of profile attrs."
|
||||
[system & {:keys [email id active? deleted?]}]
|
||||
[system & {:keys [email id active? deleted? blocked?]}]
|
||||
|
||||
(us/verify!
|
||||
:expr (some? system)
|
||||
|
@ -74,15 +74,30 @@
|
|||
:expr (or (string? email) (uuid? id))
|
||||
:hint "email or id should be provided")
|
||||
|
||||
(let [pool (:app.db/pool system)
|
||||
params (cond-> {}
|
||||
(let [params (cond-> {}
|
||||
(true? active?) (assoc :is-active true)
|
||||
(false? active?) (assoc :is-active false)
|
||||
(true? deleted?) (assoc :deleted-at (dt/now)))
|
||||
(true? deleted?) (assoc :deleted-at (dt/now))
|
||||
(true? blocked?) (assoc :is-blocked true)
|
||||
(false? blocked?) (assoc :is-blocked false))
|
||||
opts (cond-> {}
|
||||
(some? email) (assoc :email (str/lower email))
|
||||
(some? id) (assoc :id id))]
|
||||
|
||||
(some-> (db/update! pool :profile params opts)
|
||||
(profile/decode-profile-row))))
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(some-> (db/update! conn :profile params opts)
|
||||
(profile/decode-profile-row)))))
|
||||
|
||||
(defn mark-profile-as-blocked!
|
||||
"Mark the profile blocked and removes all the http sessiones
|
||||
associated with the profile-id."
|
||||
[system email]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(when-let [profile (db/get-by-params conn :profile
|
||||
{:email (str/lower email)}
|
||||
{:columns [:id :email]
|
||||
:check-not-found false})]
|
||||
(when-not (:is-blocked profile)
|
||||
(db/update! conn :profile {:is-blocked true} {:id (:id profile)})
|
||||
(db/delete! conn :http-session {:profile-id (:id profile)})
|
||||
:blocked))))
|
||||
|
|
|
@ -69,8 +69,7 @@
|
|||
|
||||
(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})
|
||||
(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]
|
||||
|
@ -85,10 +84,9 @@
|
|||
|
||||
(defmethod delete-objects "team"
|
||||
[{:keys [conn min-age storage table] :as cfg}]
|
||||
(let [sql (str/fmt sql:delete-objects
|
||||
{:table table :limit 50})
|
||||
(let [sql (str/fmt sql:delete-objects {:table table :limit 50})
|
||||
teams (db/exec! conn [sql min-age])
|
||||
storage (assoc storage :conn conn)]
|
||||
storage (media/configure-assets-storage storage conn)]
|
||||
|
||||
(doseq [{:keys [id] :as team} teams]
|
||||
(l/debug :hint "permanently delete object" :table table :id id)
|
||||
|
@ -103,32 +101,17 @@
|
|||
where deleted_at is not null
|
||||
and deleted_at < now() - ?::interval
|
||||
order by deleted_at
|
||||
limit %(limit)s
|
||||
limit ?
|
||||
for update")
|
||||
|
||||
(def sql:mark-owned-teams-deleted
|
||||
"with owned as (
|
||||
select tpr.team_id as id
|
||||
from team_profile_rel as tpr
|
||||
where tpr.is_owner is true
|
||||
and tpr.profile_id = ?
|
||||
)
|
||||
update team set deleted_at = now() - ?::interval
|
||||
where id in (select id from owned)")
|
||||
|
||||
(defmethod delete-objects "profile"
|
||||
[{:keys [conn min-age storage table] :as cfg}]
|
||||
(let [sql (str/fmt sql:retrieve-deleted-profiles {:limit 50})
|
||||
profiles (db/exec! conn [sql min-age])
|
||||
storage (assoc storage :conn conn)]
|
||||
(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 the owned teams as deleted; this enables them to be processed
|
||||
;; in the same transaction in the "team" table step.
|
||||
(db/exec-one! conn [sql:mark-owned-teams-deleted id min-age])
|
||||
|
||||
;; Mark as deleted the storage object related with the photo-id
|
||||
;; field.
|
||||
(some->> (:photo-id profile) (sto/touch-object! storage) deref)
|
||||
|
@ -164,22 +147,23 @@
|
|||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [pool] :as cfg}]
|
||||
(fn [params]
|
||||
;; Checking first on task argument allows properly testing it.
|
||||
(let [min-age (or (:min-age params) (:min-age cfg))]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (-> cfg
|
||||
(assoc :min-age (db/interval min-age))
|
||||
(assoc :conn conn))]
|
||||
(loop [tables (seq target-tables)
|
||||
total 0]
|
||||
(if-let [table (first tables)]
|
||||
(recur (rest tables)
|
||||
(+ total (process-table (assoc cfg :table table))))
|
||||
(do
|
||||
(l/info :hint "task finished" :min-age (dt/format-duration min-age) :total total)
|
||||
(db/with-atomic [conn pool]
|
||||
(let [min-age (or (:min-age params) (:min-age cfg))
|
||||
cfg (-> cfg
|
||||
(assoc :min-age (db/interval min-age))
|
||||
(assoc :conn conn))]
|
||||
(loop [tables (seq target-tables)
|
||||
total 0]
|
||||
(if-let [table (first tables)]
|
||||
(recur (rest tables)
|
||||
(+ total (process-table (assoc cfg :table table))))
|
||||
(do
|
||||
(l/info :hint "objects gc finished succesfully"
|
||||
:min-age (dt/format-duration min-age)
|
||||
:total total)
|
||||
|
||||
(when (:rollback? params)
|
||||
(db/rollback! conn))
|
||||
(when (:rollback? params)
|
||||
(db/rollback! conn))
|
||||
|
||||
{:processed total}))))))))
|
||||
{:processed total})))))))
|
||||
|
||||
|
|
|
@ -26,10 +26,14 @@
|
|||
(t/encode))]
|
||||
(jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm})))
|
||||
|
||||
(defn decode
|
||||
[{:keys [tokens-key]} token]
|
||||
(let [payload (jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm})]
|
||||
(t/decode payload)))
|
||||
|
||||
(defn verify
|
||||
[{:keys [tokens-key]} {:keys [token] :as params}]
|
||||
(let [payload (jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm})
|
||||
claims (t/decode payload)]
|
||||
[sprops {:keys [token] :as params}]
|
||||
(let [claims (decode sprops token)]
|
||||
(when (and (dt/instant? (:exp claims))
|
||||
(dt/is-before? (:exp claims) (dt/now)))
|
||||
(ex/raise :type :validation
|
||||
|
|
|
@ -11,6 +11,20 @@
|
|||
[app.common.data :as d]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defrecord WrappedValue [obj]
|
||||
clojure.lang.IDeref
|
||||
(deref [_] obj))
|
||||
|
||||
(defn wrap
|
||||
([]
|
||||
(WrappedValue. nil))
|
||||
([o]
|
||||
(WrappedValue. o)))
|
||||
|
||||
(defn wrapped?
|
||||
[o]
|
||||
(instance? WrappedValue o))
|
||||
|
||||
(defmacro defmethod
|
||||
[sname & body]
|
||||
(let [[docs body] (if (string? (first body))
|
||||
|
|
|
@ -250,9 +250,10 @@
|
|||
|
||||
(t/deftest test-allow-send-messages-predicate-with-bounces
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/mock-config-get-with
|
||||
:return (th/config-get-mock
|
||||
{:profile-bounce-threshold 3
|
||||
:profile-complaint-threshold 2})}]
|
||||
|
||||
(let [profile (th/create-profile* 1)
|
||||
pool (:app.db/pool th/*system*)]
|
||||
(th/create-complaint-for pool {:type :bounce :id (:id profile) :created-at (dt/in-past {:days 8})})
|
||||
|
@ -260,7 +261,7 @@
|
|||
(th/create-complaint-for pool {:type :bounce :id (:id profile)})
|
||||
|
||||
(t/is (true? (emails/allow-send-emails? pool profile)))
|
||||
(t/is (= 4 (:call-count (deref mock))))
|
||||
(t/is (= 4 (:call-count @mock)))
|
||||
|
||||
(th/create-complaint-for pool {:type :bounce :id (:id profile)})
|
||||
(t/is (false? (emails/allow-send-emails? pool profile))))))
|
||||
|
@ -268,7 +269,7 @@
|
|||
|
||||
(t/deftest test-allow-send-messages-predicate-with-complaints
|
||||
(with-mocks [mock {:target 'app.config/get
|
||||
:return (th/mock-config-get-with
|
||||
:return (th/config-get-mock
|
||||
{:profile-bounce-threshold 3
|
||||
:profile-complaint-threshold 2})}]
|
||||
(let [profile (th/create-profile* 1)
|
||||
|
@ -280,7 +281,7 @@
|
|||
(th/create-complaint-for pool {:type :complaint :id (:id profile)})
|
||||
|
||||
(t/is (true? (emails/allow-send-emails? pool profile)))
|
||||
(t/is (= 4 (:call-count (deref mock))))
|
||||
(t/is (= 4 (:call-count @mock)))
|
||||
|
||||
(th/create-complaint-for pool {:type :complaint :id (:id profile)})
|
||||
(t/is (false? (emails/allow-send-emails? pool profile))))))
|
||||
|
|
|
@ -537,10 +537,12 @@
|
|||
:file-id (:id file)
|
||||
:object-id frame1-id
|
||||
:components-v2 true}
|
||||
{:keys [error result] :as out} (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (= :validation (th/ex-type error)))
|
||||
(t/is (= :spec-validation (th/ex-code error)))))
|
||||
out (th/query! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
(let [{:keys [type code]} (-> out :error ex-data)]
|
||||
(t/is (= :validation type))
|
||||
(t/is (= :spec-validation code)))))
|
||||
|
||||
(t/testing "RPC :file-data-for-thumbnail"
|
||||
;; Insert a thumbnail data for the frame-id
|
||||
|
@ -728,8 +730,8 @@
|
|||
|
||||
;; Then query the specific revn
|
||||
(let [{:keys [result error] :as out} (th/query! (assoc data :revn 1))]
|
||||
(t/is (= :not-found (th/ex-type error)))
|
||||
(t/is (= :file-thumbnail-not-found (th/ex-code error)))))
|
||||
(t/is (th/ex-of-type? error :not-found))
|
||||
(t/is (th/ex-of-code? error :file-thumbnail-not-found))))
|
||||
))
|
||||
|
||||
|
||||
|
|
|
@ -119,15 +119,14 @@
|
|||
))
|
||||
|
||||
(t/deftest profile-deletion-simple
|
||||
(let [task (:app.tasks.objects-gc/handler th/*system*)
|
||||
prof (th/create-profile* 1)
|
||||
(let [prof (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
:project-id (:default-project-id prof)
|
||||
:is-shared false})]
|
||||
|
||||
;; profile is not deleted because it does not meet all
|
||||
;; conditions to be deleted.
|
||||
(let [result (task {:min-age (dt/duration 0)})]
|
||||
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
|
||||
(t/is (= 0 (:processed result))))
|
||||
|
||||
;; Request profile to be deleted
|
||||
|
@ -146,7 +145,7 @@
|
|||
(t/is (= 1 (count (:result out)))))
|
||||
|
||||
;; execute permanent deletion task
|
||||
(let [result (task {:min-age (dt/duration "-1m")})]
|
||||
(let [result (th/run-task! :objects-gc {:min-age (dt/duration "-1m")})]
|
||||
(t/is (= 1 (:processed result))))
|
||||
|
||||
;; query profile after delete
|
||||
|
@ -166,7 +165,7 @@
|
|||
(t/testing "not allowed email domain"
|
||||
(t/is (false? (cauth/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
|
||||
|
||||
(t/deftest prepare-register-and-register-profile
|
||||
(t/deftest prepare-register-and-register-profile-1
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
|
@ -195,6 +194,100 @@
|
|||
(t/is (nil? error))))
|
||||
))
|
||||
|
||||
(t/deftest prepare-register-and-register-profile-1
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
out (th/mutation! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (string? token))
|
||||
|
||||
|
||||
;; try register without token
|
||||
(let [data {::th/type :register-profile
|
||||
:fullname "foobar"
|
||||
:accept-terms-and-privacy true}
|
||||
out (th/mutation! data)]
|
||||
(let [error (:error out)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :spec-validation))))
|
||||
|
||||
;; try correct register
|
||||
(let [data {::th/type :register-profile
|
||||
:token token
|
||||
:fullname "foobar"
|
||||
:accept-terms-and-privacy true
|
||||
:accept-newsletter-subscription true}]
|
||||
(let [{:keys [result error]} (th/mutation! data)]
|
||||
(t/is (nil? error))))
|
||||
))
|
||||
|
||||
(t/deftest prepare-register-and-register-profile-2
|
||||
(with-redefs [app.rpc.commands.auth/register-retry-threshold (dt/duration 500)]
|
||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||
(let [current-token (atom nil)]
|
||||
|
||||
;; PREPARE REGISTER
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)
|
||||
token (get-in out [:result :token])]
|
||||
(t/is (string? token))
|
||||
(reset! current-token token))
|
||||
|
||||
;; DO REGISTRATION: try correct register attempt 1
|
||||
(let [data {::th/type :register-profile
|
||||
:token @current-token
|
||||
:fullname "foobar"
|
||||
:accept-terms-and-privacy true
|
||||
:accept-newsletter-subscription true}
|
||||
out (th/command! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= 1 (:call-count @mock))))
|
||||
|
||||
(th/reset-mock! mock)
|
||||
|
||||
;; PREPARE REGISTER without waiting for threshold
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :validation (-> out :error th/ex-type)))
|
||||
(t/is (= :email-already-exists (-> out :error th/ex-code))))
|
||||
|
||||
(th/sleep {:millis 500})
|
||||
(th/reset-mock! mock)
|
||||
|
||||
;; PREPARE REGISTER waiting the threshold
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "hello@example.com"
|
||||
:password "foobar"}
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 0 (:call-count @mock)))
|
||||
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result :token))
|
||||
(reset! current-token (:token result))))
|
||||
|
||||
;; DO REGISTRATION: try correct register attempt 1
|
||||
(let [data {::th/type :register-profile
|
||||
:token @current-token
|
||||
:fullname "foobar"
|
||||
:accept-terms-and-privacy true
|
||||
:accept-newsletter-subscription true}
|
||||
out (th/command! data)]
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count @mock))))
|
||||
|
||||
))
|
||||
))
|
||||
|
||||
|
||||
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-1
|
||||
(with-redefs [app.config/flags [:disable-registration]]
|
||||
(let [sprops (:app.setup/props th/*system*)
|
||||
|
@ -239,34 +332,39 @@
|
|||
:invitation-token itoken
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
{:keys [result error] :as out} (th/mutation! data)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :email-does-not-match-invitation (th/ex-code error))))))
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :restriction (:type edata)))
|
||||
(t/is (= :email-does-not-match-invitation (:code edata))))
|
||||
)))
|
||||
|
||||
(t/deftest prepare-register-with-registration-disabled
|
||||
(th/with-mocks {#'app.config/flags nil}
|
||||
(with-redefs [app.config/flags #{}]
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
:password "foobar"}]
|
||||
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :restriction))
|
||||
(t/is (th/ex-of-code? error :registration-disabled))))))
|
||||
:password "foobar"}
|
||||
out (th/command! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :restriction (:type edata)))
|
||||
(t/is (= :registration-disabled (:code edata)))))))
|
||||
|
||||
(t/deftest prepare-register-with-existing-user
|
||||
(let [profile (th/create-profile* 1)
|
||||
data {::th/type :prepare-register-profile
|
||||
:email (:email profile)
|
||||
:password "foobar"}]
|
||||
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :email-already-exists)))))
|
||||
:password "foobar"}
|
||||
out (th/command! data)]
|
||||
|
||||
(t/deftest test-register-profile-with-bounced-email
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :email-already-exists (:code edata))))))
|
||||
|
||||
(t/deftest register-profile-with-bounced-email
|
||||
(let [pool (:app.db/pool th/*system*)
|
||||
data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
|
@ -274,34 +372,38 @@
|
|||
|
||||
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
||||
|
||||
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :email-has-permanent-bounces)))))
|
||||
(let [out (th/command! data)]
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :email-has-permanent-bounces (:code edata)))))))
|
||||
|
||||
(t/deftest test-register-profile-with-complained-email
|
||||
(t/deftest register-profile-with-complained-email
|
||||
(let [pool (:app.db/pool th/*system*)
|
||||
data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
:password "foobar"}]
|
||||
|
||||
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
|
||||
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||
(t/is (nil? error))
|
||||
(t/is (string? (:token result))))))
|
||||
|
||||
(t/deftest test-register-profile-with-email-as-password
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
:password "USER@example.com"}]
|
||||
(let [out (th/command! data)]
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (contains? result :token))))))
|
||||
|
||||
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :email-as-password)))))
|
||||
(t/deftest register-profile-with-email-as-password
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
:email "user@example.com"
|
||||
:password "USER@example.com"}
|
||||
out (th/command! data)]
|
||||
|
||||
(t/deftest test-email-change-request
|
||||
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}]
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :email-as-password (:code edata))))))
|
||||
|
||||
(t/deftest email-change-request
|
||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||
(let [profile (th/create-profile* 1)
|
||||
pool (:app.db/pool th/*system*)
|
||||
data {::th/type :request-email-change
|
||||
|
@ -312,7 +414,7 @@
|
|||
(let [out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(let [mock (deref email-send-mock)]
|
||||
(let [mock @mock]
|
||||
(t/is (= 1 (:call-count mock)))
|
||||
(t/is (true? (:called? mock)))))
|
||||
|
||||
|
@ -321,7 +423,7 @@
|
|||
(let [out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (= 2 (:call-count (deref email-send-mock)))))
|
||||
(t/is (= 2 (:call-count @mock))))
|
||||
|
||||
;; with bounces
|
||||
(th/create-global-complaint-for pool {:type :bounce :email (:email data)})
|
||||
|
@ -331,28 +433,26 @@
|
|||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
|
||||
(t/is (= 2 (:call-count (deref email-send-mock))))))))
|
||||
(t/is (= 2 (:call-count @mock)))))))
|
||||
|
||||
|
||||
(t/deftest test-email-change-request-without-smtp
|
||||
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}]
|
||||
(t/deftest email-change-request-without-smtp
|
||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||
(with-redefs [app.config/flags #{}]
|
||||
(let [profile (th/create-profile* 1)
|
||||
pool (:app.db/pool th/*system*)
|
||||
data {::th/type :request-email-change
|
||||
:profile-id (:id profile)
|
||||
:email "user1@example.com"}]
|
||||
:email "user1@example.com"}
|
||||
out (th/mutation! data)]
|
||||
|
||||
(let [out (th/mutation! data)
|
||||
res (:result out)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (= {:changed true} res))
|
||||
(let [mock (deref email-send-mock)]
|
||||
(t/is (false? (:called? mock)))))))))
|
||||
;; (th/print-result! out)
|
||||
(t/is (false? (:called? @mock)))
|
||||
(let [res (:result out)]
|
||||
(t/is (= {:changed true} res)))))))
|
||||
|
||||
|
||||
(t/deftest test-request-profile-recovery
|
||||
(t/deftest request-profile-recovery
|
||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||
(let [profile1 (th/create-profile* 1)
|
||||
profile2 (th/create-profile* 2 {:is-active true})
|
||||
|
@ -363,13 +463,13 @@
|
|||
(let [data (assoc data :email "foo@bar.com")
|
||||
out (th/mutation! data)]
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (= 0 (:call-count (deref mock)))))
|
||||
(t/is (= 0 (:call-count @mock))))
|
||||
|
||||
;; with valid email inactive user
|
||||
(let [data (assoc data :email (:email profile1))
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
(t/is (= 0 (:call-count (deref mock))))
|
||||
(t/is (= 0 (:call-count @mock)))
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :profile-not-verified)))
|
||||
|
@ -379,7 +479,7 @@
|
|||
out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (= 1 (:call-count (deref mock)))))
|
||||
(t/is (= 1 (:call-count @mock))))
|
||||
|
||||
;; with valid email and active user with global complaints
|
||||
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
|
||||
|
@ -387,7 +487,7 @@
|
|||
out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (= 2 (:call-count (deref mock)))))
|
||||
(t/is (= 2 (:call-count @mock))))
|
||||
|
||||
;; with valid email and active user with global bounce
|
||||
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
|
||||
|
@ -395,7 +495,7 @@
|
|||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (= 2 (:call-count (deref mock))))
|
||||
(t/is (= 2 (:call-count @mock)))
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :email-has-permanent-bounces)))
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
[app.http :as http]
|
||||
[app.storage :as sto]
|
||||
[app.test-helpers :as th]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.time :as dt]
|
||||
[clojure.test :as t]
|
||||
[datoteka.core :as fs]
|
||||
|
@ -19,7 +20,7 @@
|
|||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest test-invite-team-member
|
||||
(t/deftest invite-team-member
|
||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||
profile2 (th/create-profile* 2 {:is-active true})
|
||||
|
@ -34,17 +35,16 @@
|
|||
:profile-id (:id profile1)}]
|
||||
|
||||
;; invite external user without complaints
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
out (th/mutation! data)
|
||||
;;retrieve the value from the database and check its content
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
out (th/mutation! data)
|
||||
;; retrieve the value from the database and check its content
|
||||
invitation (db/exec-one!
|
||||
th/*pool*
|
||||
["select count(*) as num from team_invitation where team_id = ? and email_to = ?"
|
||||
(:team-id data) "foo@bar.com"])]
|
||||
th/*pool*
|
||||
["select count(*) as num from team_invitation where team_id = ? and email_to = ?"
|
||||
(:team-id data) "foo@bar.com"])]
|
||||
|
||||
;; (th/print-result! out)
|
||||
|
||||
(t/is (= {} (:result out)))
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count (deref mock))))
|
||||
(t/is (= 1 (:num invitation))))
|
||||
|
||||
|
@ -52,7 +52,7 @@
|
|||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/mutation! data)]
|
||||
(t/is (= {} (:result out)))
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count (deref mock)))))
|
||||
|
||||
;; invite user with complaint
|
||||
|
@ -60,35 +60,183 @@
|
|||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
out (th/mutation! data)]
|
||||
(t/is (= {} (:result out)))
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count (deref mock)))))
|
||||
|
||||
;; invite user with bounce
|
||||
(th/reset-mock! mock)
|
||||
|
||||
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
|
||||
(let [data (assoc data :email "foo@bar.com")
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
out (th/mutation! data)]
|
||||
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
|
||||
(t/is (= 0 (:call-count (deref mock)))))
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= 0 (:call-count @mock)))
|
||||
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :email-has-permanent-bounces (:code edata)))))
|
||||
|
||||
;; invite internal user that is muted
|
||||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email (:email profile3))
|
||||
out (th/mutation! data)
|
||||
error (:error out)]
|
||||
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (th/ex-of-type? error :validation))
|
||||
(t/is (th/ex-of-code? error :member-is-muted))
|
||||
(t/is (= 0 (:call-count (deref mock)))))
|
||||
(let [data (assoc data :email (:email profile3))
|
||||
out (th/mutation! data)]
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= 0 (:call-count @mock)))
|
||||
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :member-is-muted (:code edata)))))
|
||||
|
||||
)))
|
||||
|
||||
|
||||
(t/deftest invitation-tokens
|
||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||
profile2 (th/create-profile* 2 {:is-active true})
|
||||
|
||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||
|
||||
sprops (:app.setup/props th/*system*)
|
||||
pool (:app.db/pool th/*system*)]
|
||||
|
||||
;; Try to invite a not existing user
|
||||
(let [data {::th/type :invite-team-member
|
||||
:email "notexisting@example.com"
|
||||
:team-id (:id team)
|
||||
:role :editor
|
||||
:profile-id (:id profile1)}
|
||||
out (th/mutation! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count @mock)))
|
||||
(t/is (= 1 (-> out :result count)))
|
||||
|
||||
(let [token (-> out :result first)
|
||||
claims (tokens/decode sprops token)]
|
||||
(t/is (= :team-invitation (:iss claims)))
|
||||
(t/is (= (:id profile1) (:profile-id claims)))
|
||||
(t/is (= :editor (:role claims)))
|
||||
(t/is (= (:id team) (:team-id claims)))
|
||||
(t/is (= (:email data) (:member-email claims)))
|
||||
(t/is (nil? (:member-id claims)))))
|
||||
|
||||
(th/reset-mock! mock)
|
||||
|
||||
;; Try to invite existing user
|
||||
(let [data {::th/type :invite-team-member
|
||||
:email (:email profile2)
|
||||
:team-id (:id team)
|
||||
:role :editor
|
||||
:profile-id (:id profile1)}
|
||||
out (th/mutation! data)]
|
||||
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 1 (:call-count @mock)))
|
||||
(t/is (= 1 (-> out :result count)))
|
||||
|
||||
(let [token (-> out :result first)
|
||||
claims (tokens/decode sprops token)]
|
||||
(t/is (= :team-invitation (:iss claims)))
|
||||
(t/is (= (:id profile1) (:profile-id claims)))
|
||||
(t/is (= :editor (:role claims)))
|
||||
(t/is (= (:id team) (:team-id claims)))
|
||||
(t/is (= (:email data) (:member-email claims)))
|
||||
(t/is (= (:id profile2) (:member-id claims)))))
|
||||
|
||||
)))
|
||||
|
||||
|
||||
(t/deftest accept-invitation-tokens
|
||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||
profile2 (th/create-profile* 2 {:is-active true})
|
||||
|
||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||
|
||||
sprops (:app.setup/props th/*system*)
|
||||
pool (:app.db/pool th/*system*)]
|
||||
|
||||
(let [token (tokens/generate sprops
|
||||
{:iss :team-invitation
|
||||
:exp (dt/in-future "1h")
|
||||
:profile-id (:id profile1)
|
||||
:role :editor
|
||||
:team-id (:id team)
|
||||
:member-email (:email profile2)
|
||||
:member-id (:id profile2)})]
|
||||
|
||||
;; --- Verify token as anonymous user
|
||||
|
||||
(db/insert! pool :team-invitation
|
||||
{:team-id (:id team)
|
||||
:email-to (:email profile2)
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(let [data {::th/type :verify-token :token token}
|
||||
out (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (= :created (:state result)))
|
||||
(t/is (= (:email profile2) (:member-email result)))
|
||||
(t/is (= (:id profile2) (:member-id result))))
|
||||
|
||||
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
|
||||
(t/is (= 2 (count rows)))))
|
||||
|
||||
;; Clean members
|
||||
(db/delete! pool :team-profile-rel
|
||||
{:team-id (:id team)
|
||||
:profile-id (:id profile2)})
|
||||
|
||||
|
||||
;; --- Verify token as logged-in user
|
||||
|
||||
(db/insert! pool :team-invitation
|
||||
{:team-id (:id team)
|
||||
:email-to (:email profile2)
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(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)]
|
||||
(t/is (= :created (:state result)))
|
||||
(t/is (= (:email profile2) (:member-email result)))
|
||||
(t/is (= (:id profile2) (:member-id result))))
|
||||
|
||||
(let [rows (db/query pool :team-profile-rel {:team-id (:id team)})]
|
||||
(t/is (= 2 (count rows)))))
|
||||
|
||||
|
||||
;; --- Verify token as logged-in wrong user
|
||||
|
||||
(db/insert! pool :team-invitation
|
||||
{:team-id (:id team)
|
||||
:email-to (:email profile2)
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(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)]
|
||||
(t/is (= :validation (:type edata)))
|
||||
(t/is (= :invalid-token (:code edata)))))
|
||||
|
||||
)))
|
||||
|
||||
|
||||
|
||||
(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})
|
||||
|
@ -108,20 +256,17 @@
|
|||
(th/reset-mock! mock)
|
||||
(let [data (assoc data :email (:email profile2))
|
||||
out (th/mutation! data)]
|
||||
(t/is (= {} (:result out)))
|
||||
(t/is (th/success? out))
|
||||
(t/is (= 0 (:call-count (deref mock)))))
|
||||
|
||||
|
||||
(let [members (db/query pool :team-profile-rel
|
||||
{:team-id (:id team)
|
||||
:profile-id (:id profile2)})]
|
||||
(t/is (= 1 (count members)))
|
||||
(t/is (true? (-> members first :can-edit))))))))
|
||||
|
||||
|
||||
(t/deftest test-deletion
|
||||
(let [task (:app.tasks.objects-gc/handler th/*system*)
|
||||
profile1 (th/create-profile* 1 {:is-active true})
|
||||
(t/deftest team-deletion
|
||||
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||
pool (:app.db/pool th/*system*)
|
||||
data {::th/type :delete-team
|
||||
|
@ -130,7 +275,7 @@
|
|||
|
||||
;; team is not deleted because it does not meet all
|
||||
;; conditions to be deleted.
|
||||
(let [result (task {:min-age (dt/duration 0)})]
|
||||
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
|
||||
(t/is (= 0 (:processed result))))
|
||||
|
||||
;; query the list of teams
|
||||
|
@ -138,7 +283,7 @@
|
|||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (= 2 (count result)))
|
||||
(t/is (= (:id team) (get-in result [1 :id])))
|
||||
|
@ -149,21 +294,20 @@
|
|||
:id (:id team)
|
||||
:profile-id (:id profile1)}
|
||||
out (th/mutation! params)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out))))
|
||||
(t/is (th/success? out)))
|
||||
|
||||
;; query the list of teams after soft deletion
|
||||
(let [data {::th/type :teams
|
||||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)]
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
|
||||
|
||||
;; run permanent deletion (should be noop)
|
||||
(let [result (task {:min-age (dt/duration {:minutes 1})})]
|
||||
(let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
|
||||
(t/is (= 0 (:processed result))))
|
||||
|
||||
;; query the list of projects after hard deletion
|
||||
|
@ -172,13 +316,12 @@
|
|||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(let [error (:error out)
|
||||
error-data (ex-data error)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (= (:type error-data) :not-found))))
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :not-found (:type edata)))))
|
||||
|
||||
;; run permanent deletion
|
||||
(let [result (task {:min-age (dt/duration 0)})]
|
||||
(let [result (th/run-task! :objects-gc {:min-age (dt/duration 0)})]
|
||||
(t/is (= 1 (:processed result))))
|
||||
|
||||
;; query the list of projects of a after hard deletion
|
||||
|
@ -187,31 +330,27 @@
|
|||
:profile-id (:id profile1)}
|
||||
out (th/query! data)]
|
||||
;; (th/print-result! out)
|
||||
(let [error (:error out)
|
||||
error-data (ex-data error)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (= (:type error-data) :not-found))))
|
||||
|
||||
(t/is (not (th/success? out)))
|
||||
(let [edata (-> out :error ex-data)]
|
||||
(t/is (= :not-found (:type edata)))))
|
||||
))
|
||||
|
||||
|
||||
|
||||
|
||||
(t/deftest query-team-invitations
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||
data {::th/type :team-invitations
|
||||
:profile-id (:id prof)
|
||||
:team-id (:id team)}]
|
||||
|
||||
;;insert an entry on the database with an enabled invitation
|
||||
;; insert an entry on the database with an enabled invitation
|
||||
(db/insert! th/*pool* :team-invitation
|
||||
{:team-id (:team-id data)
|
||||
:email-to "test1@mail.com"
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
{:team-id (:team-id data)
|
||||
:email-to "test1@mail.com"
|
||||
:role "editor"
|
||||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
|
||||
;;insert an entry on the database with an expired invitation
|
||||
;; insert an entry on the database with an expired invitation
|
||||
(db/insert! th/*pool* :team-invitation
|
||||
{:team-id (:team-id data)
|
||||
:email-to "test2@mail.com"
|
||||
|
@ -219,27 +358,26 @@
|
|||
:valid-until (dt/in-past "48h")})
|
||||
|
||||
(let [out (th/query! data)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (th/success? out))
|
||||
(let [result (:result out)
|
||||
one (first result)
|
||||
two (second result)]
|
||||
one (first result)
|
||||
two (second result)]
|
||||
(t/is (= 2 (count result)))
|
||||
(t/is (= "test1@mail.com" (:email one)))
|
||||
(t/is (= "test2@mail.com" (:email two)))
|
||||
(t/is (false? (:expired one)))
|
||||
(t/is (true? (:expired two)))))))
|
||||
|
||||
|
||||
(t/deftest update-team-invitation-role
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||
data {::th/type :update-team-invitation-role
|
||||
:profile-id (:id prof)
|
||||
:team-id (:id team)
|
||||
:email "TEST1@mail.com"
|
||||
:role :admin}]
|
||||
|
||||
;;insert an entry on the database with an invitation
|
||||
;; insert an entry on the database with an invitation
|
||||
(db/insert! th/*pool* :team-invitation
|
||||
{:team-id (:team-id data)
|
||||
:email-to "test1@mail.com"
|
||||
|
@ -247,24 +385,22 @@
|
|||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(let [out (th/mutation! data)
|
||||
;;retrieve the value from the database and check its content
|
||||
result (db/get-by-params th/*pool* :team-invitation
|
||||
{:team-id (:team-id data) :email-to "test1@mail.com"}
|
||||
{:check-not-found false})]
|
||||
(t/is (nil? (:error out)))
|
||||
;; retrieve the value from the database and check its content
|
||||
res (db/get* th/*pool* :team-invitation
|
||||
{:team-id (:team-id data) :email-to "test1@mail.com"})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (= "admin" (:role result))))))
|
||||
|
||||
(t/is (= "admin" (:role res))))))
|
||||
|
||||
(t/deftest delete-team-invitation
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team (th/create-team* 1 {:profile-id (:id prof)})
|
||||
data {::th/type :delete-team-invitation
|
||||
:profile-id (:id prof)
|
||||
:team-id (:id team)
|
||||
:email "TEST1@mail.com"}]
|
||||
|
||||
;;insert an entry on the database with an invitation
|
||||
;; insert an entry on the database with an invitation
|
||||
(db/insert! th/*pool* :team-invitation
|
||||
{:team-id (:team-id data)
|
||||
:email-to "test1@mail.com"
|
||||
|
@ -272,10 +408,10 @@
|
|||
:valid-until (dt/in-future "48h")})
|
||||
|
||||
(let [out (th/mutation! data)
|
||||
;;retrieve the value from the database and check its content
|
||||
result (db/get-by-params th/*pool* :team-invitation
|
||||
{:team-id (:team-id data) :email-to "test1@mail.com"}
|
||||
{:check-not-found false})]
|
||||
(t/is (nil? (:error out)))
|
||||
;; retrieve the value from the database and check its content
|
||||
res (db/get* th/*pool* :team-invitation
|
||||
{:team-id (:team-id data) :email-to "test1@mail.com"})]
|
||||
|
||||
(t/is (th/success? out))
|
||||
(t/is (nil? (:result out)))
|
||||
(t/is (nil? result)))))
|
||||
(t/is (nil? res)))))
|
||||
|
|
|
@ -23,9 +23,11 @@
|
|||
[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]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.test :as t]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs]
|
||||
[environ.core :refer [env]]
|
||||
|
@ -277,8 +279,10 @@
|
|||
(defmacro try-on!
|
||||
[expr]
|
||||
`(try
|
||||
{:error nil
|
||||
:result (deref ~expr)}
|
||||
(let [result# (deref ~expr)
|
||||
result# (cond-> result# (sv/wrapped? result#) deref)]
|
||||
{:error nil
|
||||
:result result#})
|
||||
(catch Exception e#
|
||||
{:error (handle-error e#)
|
||||
:result nil})))
|
||||
|
@ -299,6 +303,14 @@
|
|||
(let [method-fn (get-in *system* [:app.rpc/methods :queries type])]
|
||||
(try-on! (method-fn (dissoc data ::type)))))
|
||||
|
||||
(defn run-task!
|
||||
([name]
|
||||
(run-task! name {}))
|
||||
([name params]
|
||||
(let [tasks (:app.worker/registry *system*)]
|
||||
(let [task-fn (get tasks name)]
|
||||
(task-fn params)))))
|
||||
|
||||
;; --- UTILS
|
||||
|
||||
(defn print-error!
|
||||
|
@ -358,6 +370,10 @@
|
|||
(let [data (ex-data e)]
|
||||
(= code (:code data))))
|
||||
|
||||
(defn success?
|
||||
[{:keys [result error]}]
|
||||
(nil? error))
|
||||
|
||||
(defn tempfile
|
||||
[source]
|
||||
(let [rsc (io/resource source)
|
||||
|
@ -366,29 +382,6 @@
|
|||
(io/file tmp))
|
||||
tmp))
|
||||
|
||||
(defn sleep
|
||||
[ms]
|
||||
(Thread/sleep ms))
|
||||
|
||||
(defn mock-config-get-with
|
||||
"Helper for mock app.config/get"
|
||||
[data]
|
||||
(fn
|
||||
([key]
|
||||
(get data key (get cf/config key)))
|
||||
([key default]
|
||||
(get data key (get cf/config key default)))))
|
||||
|
||||
|
||||
(defmacro with-mocks
|
||||
[rebinds & body]
|
||||
`(with-redefs-fn ~rebinds
|
||||
(fn [] ~@body)))
|
||||
|
||||
(defn reset-mock!
|
||||
[m]
|
||||
(reset! m @(mk/make-mock {})))
|
||||
|
||||
(defn pause
|
||||
[]
|
||||
(let [^java.io.Console cnsl (System/console)]
|
||||
|
@ -408,3 +401,18 @@
|
|||
[& params]
|
||||
(apply db/query *pool* params))
|
||||
|
||||
(defn sleep
|
||||
[ms-or-duration]
|
||||
(Thread/sleep (inst-ms (dt/duration ms-or-duration))))
|
||||
|
||||
(defn config-get-mock
|
||||
[data]
|
||||
(fn
|
||||
([key]
|
||||
(get data key (get cf/config key)))
|
||||
([key default]
|
||||
(get data key (get cf/config key default)))))
|
||||
|
||||
(defn reset-mock!
|
||||
[m]
|
||||
(reset! m @(mk/make-mock {})))
|
||||
|
|
|
@ -3,22 +3,22 @@
|
|||
org.clojure/data.json {:mvn/version "2.4.0"}
|
||||
org.clojure/tools.cli {:mvn/version "1.0.206"}
|
||||
metosin/jsonista {:mvn/version "0.3.6"}
|
||||
org.clojure/clojurescript {:mvn/version "1.11.57"}
|
||||
org.clojure/clojurescript {:mvn/version "1.11.60"}
|
||||
|
||||
;; Logging
|
||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.17.2"}
|
||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.17.2"}
|
||||
org.apache.logging.log4j/log4j-web {:mvn/version "2.17.2"}
|
||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.17.2"}
|
||||
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.17.2"}
|
||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.19.0"}
|
||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.19.0"}
|
||||
org.apache.logging.log4j/log4j-web {:mvn/version "2.19.0"}
|
||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.19.0"}
|
||||
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.18.0"}
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"}
|
||||
|
||||
selmer/selmer {:mvn/version "1.12.51"}
|
||||
selmer/selmer {:mvn/version "1.12.55"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
|
||||
expound/expound {:mvn/version "0.9.0"}
|
||||
com.cognitect/transit-clj {:mvn/version "1.0.329"}
|
||||
com.cognitect/transit-cljs {:mvn/version "0.8.269"}
|
||||
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
funcool/promesa {:mvn/version "8.0.450"}
|
||||
|
@ -42,21 +42,22 @@
|
|||
{:extra-deps
|
||||
{org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
org.clojure/test.check {:mvn/version "RELEASE"}
|
||||
thheller/shadow-cljs {:mvn/version "2.19.8"}
|
||||
thheller/shadow-cljs {:mvn/version "2.20.2"}
|
||||
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
criterium/criterium {:mvn/version "RELEASE"}
|
||||
mockery/mockery {:mvn/version "RELEASE"}}
|
||||
:extra-paths ["test" "dev"]}
|
||||
|
||||
:build
|
||||
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.1" :git/sha "7d40500"}}
|
||||
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.3" :git/sha "0d20256"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
{:extra-paths ["test"]
|
||||
:extra-deps
|
||||
{io.github.cognitect-labs/test-runner
|
||||
{:git/tag "v0.5.0" :git/sha "b3fd0d2"}}
|
||||
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
|
||||
:main-opts ["-m" "cognitect.test-runner"]
|
||||
:exec-fn cognitect.test-runner.api/test}
|
||||
|
||||
:shadow-cljs
|
||||
|
|
|
@ -107,12 +107,12 @@ 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'; \
|
||||
ESUM='c640fc5e5710dba3f92099a791be50fab54f91cf2c3838cb536ded27ecc562a6'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu19.28.81-ca-jdk19.0.0-linux_aarch64.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'; \
|
||||
ESUM='6813da339124261092daab369a1c60dea5f27f4ba9608a16517191d30511a087'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu19.28.81-ca-jdk19.0.0-linux_x64.tar.gz'; \
|
||||
;; \
|
||||
*) \
|
||||
echo "Unsupported arch: ${ARCH}"; \
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
PENPOT_PUBLIC_URI=http://localhost:9001
|
||||
|
||||
## Feature flags.
|
||||
|
||||
PENPOT_FLAGS="enable-registration enable-login disable-email-verification"
|
||||
PENPOT_FLAGS=enable-registration enable-login disable-email-verification
|
||||
|
||||
## Temporal workaround because of bad builtin default
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
:dev
|
||||
{:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "2.19.8"}}}
|
||||
{thheller/shadow-cljs {:mvn/version "2.20.2"}}}
|
||||
|
||||
:shadow-cljs
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"xregexp": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shadow-cljs": "^2.19.8",
|
||||
"shadow-cljs": "^2.20.2",
|
||||
"source-map-support": "^0.5.21"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
["process" :as proc]
|
||||
[app.browser :as bwr]
|
||||
[app.common.logging :as l]
|
||||
[app.config]
|
||||
[app.config :as cf]
|
||||
[app.http :as http]
|
||||
[app.redis :as redis]
|
||||
[promesa.core :as p]))
|
||||
|
@ -19,7 +19,9 @@
|
|||
|
||||
(defn start
|
||||
[& _]
|
||||
(l/info :msg "initializing")
|
||||
(l/info :msg "initializing"
|
||||
:public-uri (str (cf/get :public-uri))
|
||||
:version (:full @cf/version))
|
||||
(p/do!
|
||||
(bwr/init)
|
||||
(redis/init)
|
||||
|
@ -39,5 +41,6 @@
|
|||
(http/stop)
|
||||
(done)))
|
||||
|
||||
(proc/on "uncaughtException" (fn [cause]
|
||||
(js/console.error cause)))
|
||||
(proc/on "uncaughtException"
|
||||
(fn [cause]
|
||||
(js/console.error cause)))
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
:name (:name resource)
|
||||
:status "ended"}))))
|
||||
on-error (fn [cause]
|
||||
(l/error :hint "unexpected error happened on export multiple process"
|
||||
(l/error :hint "unexpected error on export multiple"
|
||||
:cause cause)
|
||||
(if wait
|
||||
(p/rejected cause)
|
||||
|
|
|
@ -90,12 +90,13 @@
|
|||
(fn [{:keys [:response/body :response/status] :as exchange}]
|
||||
(cond
|
||||
(map? body)
|
||||
(let [data (t/encode-str body {:type :json-verbose})]
|
||||
(let [data (t/encode-str body {:type :json-verbose})
|
||||
size (js/Buffer.byteLength data "utf-8")]
|
||||
(-> exchange
|
||||
(assoc :response/body data)
|
||||
(assoc :response/status 200)
|
||||
(update :response/headers assoc "content-type" "application/transit+json")
|
||||
(update :response/headers assoc "content-length" (count data))))
|
||||
(update :response/headers assoc "content-length" size)))
|
||||
|
||||
(and (nil? body)
|
||||
(= 200 status))
|
||||
|
|
|
@ -1098,10 +1098,10 @@ shadow-cljs-jar@1.3.2:
|
|||
resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
|
||||
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
|
||||
|
||||
shadow-cljs@^2.19.8:
|
||||
version "2.19.8"
|
||||
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.19.8.tgz#1ce96cab3e4903bed8d401ffbe88b8939f5454d3"
|
||||
integrity sha512-6qek3mcAP0hrnC5FxrTebBrgLGpOuhlnp06vdxp6g0M5Gl6w2Y0hzSwa1s2K8fMOkzE4/ciQor75b2y64INgaw==
|
||||
shadow-cljs@^2.20.2:
|
||||
version "2.20.2"
|
||||
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.20.2.tgz#24a4b204f1f2288dc4ff2d0a4f3972a6e5307645"
|
||||
integrity sha512-2kzWnV1QM6KBetziCAkCf8BJdnDX2CwiAr4yhvOsiQpaNJcMzwMsJTX/gTHz58yQg0dV5uwPsIyBlvyIfl30rg==
|
||||
dependencies:
|
||||
node-libs-browser "^2.2.1"
|
||||
readline-sync "^1.4.7"
|
||||
|
|
|
@ -10,9 +10,13 @@
|
|||
funcool/beicon {:mvn/version "2021.07.05-1"}
|
||||
funcool/okulary {:mvn/version "2022.04.11-16"}
|
||||
funcool/potok {:mvn/version "2022.04.28-67"}
|
||||
funcool/rumext {:mvn/version "2022.04.19-148"}
|
||||
funcool/tubax {:mvn/version "2021.05.20-0"}
|
||||
|
||||
funcool/rumext
|
||||
{:git/tag "v2.0"
|
||||
:git/sha "fc617a8"
|
||||
:git/url "https://github.com/funcool/rumext.git"}
|
||||
|
||||
instaparse/instaparse {:mvn/version "1.4.12"}
|
||||
garden/garden {:git/url "https://github.com/noprompt/garden"
|
||||
:git/sha "05590ecb5f6fa670856f3d1ab400aa4961047480"}
|
||||
|
@ -32,7 +36,7 @@
|
|||
:dev
|
||||
{:extra-paths ["dev"]
|
||||
:extra-deps
|
||||
{thheller/shadow-cljs {:mvn/version "2.19.9"}
|
||||
{thheller/shadow-cljs {:mvn/version "2.20.2"}
|
||||
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
cider/cider-nrepl {:mvn/version "0.28.4"}}}
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.0",
|
||||
"sass": "^1.53.0",
|
||||
"shadow-cljs": "2.19.9"
|
||||
"shadow-cljs": "2.20.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^6.17.4",
|
||||
|
|
|
@ -88,7 +88,6 @@
|
|||
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||
(def translations (obj/get global "penpotTranslations"))
|
||||
(def themes (obj/get global "penpotThemes"))
|
||||
(def sentry-dsn (obj/get global "penpotSentryDsn"))
|
||||
(def onboarding-form-id (obj/get global "penpotOnboardingQuestionsFormId"))
|
||||
|
||||
(def build-date (parse-build-date global))
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
[app.main.data.users :as du]
|
||||
[app.main.data.websocket :as ws]
|
||||
[app.main.errors]
|
||||
[app.main.sentry :as sentry]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui :as ui]
|
||||
[app.main.ui.alert]
|
||||
|
@ -29,7 +28,7 @@
|
|||
[debug]
|
||||
[features]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(log/initialize!)
|
||||
(log/set-level! :root :warn)
|
||||
|
@ -75,7 +74,6 @@
|
|||
(defn ^:export init
|
||||
[]
|
||||
(worker/init!)
|
||||
(sentry/init!)
|
||||
(i18n/init! cf/translations)
|
||||
(theme/init! cf/themes)
|
||||
(init-ui)
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
[app.main.store :as st]
|
||||
[okulary.core :as l]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(log/set-level! :debug)
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
[beicon.core :as rx]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:const viewbox-decimal-precision 3)
|
||||
(def ^:private default-color clr/canvas)
|
||||
|
|
|
@ -1,60 +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.main.sentry
|
||||
"Sentry integration."
|
||||
(:require
|
||||
["@sentry/browser" :as sentry]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.refs :as refs]))
|
||||
|
||||
(defn- setup-profile!
|
||||
[profile]
|
||||
(if (or (= uuid/zero (:id profile))
|
||||
(nil? profile))
|
||||
(sentry/setUser nil)
|
||||
(sentry/setUser #js {:id (str (:id profile))})))
|
||||
|
||||
(defn init!
|
||||
[]
|
||||
(setup-profile! @refs/profile)
|
||||
(when cf/sentry-dsn
|
||||
(sentry/init
|
||||
#js {:dsn cf/sentry-dsn
|
||||
:autoSessionTracking false
|
||||
:attachStacktrace false
|
||||
:release (str "frontend@" (:base @cf/version))
|
||||
:maxBreadcrumbs 20
|
||||
:beforeBreadcrumb (fn [breadcrumb _hint]
|
||||
(let [category (.-category ^js breadcrumb)]
|
||||
(if (= category "navigate")
|
||||
breadcrumb
|
||||
nil)))
|
||||
:tracesSampleRate 1.0})
|
||||
|
||||
(add-watch refs/profile ::profile
|
||||
(fn [_ _ _ profile]
|
||||
(setup-profile! profile)))
|
||||
|
||||
(add-watch refs/route ::route
|
||||
(fn [_ _ _ route]
|
||||
(sentry/addBreadcrumb
|
||||
#js {:category "navigate",
|
||||
:message (str "path: " (:path route))
|
||||
:level (.-Info ^js sentry/Severity)})))))
|
||||
|
||||
(defn capture-exception
|
||||
[err]
|
||||
(when cf/sentry-dsn
|
||||
(when (ex/ex-info? err)
|
||||
(sentry/setContext "ex-data", (clj->js (ex-data err))))
|
||||
(sentry/captureException err))
|
||||
err)
|
||||
|
||||
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
[app.main.ui.viewer :as viewer]
|
||||
[app.main.ui.workspace :as workspace]
|
||||
[app.util.router :as rt]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc on-main-error
|
||||
[{:keys [error] :as props}]
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr t]]
|
||||
[app.util.keyboard :as k]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(mf/defc alert-dialog
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc terms-login
|
||||
[]
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def show-alt-login-buttons?
|
||||
(some (partial contains? @cf/flags)
|
||||
|
@ -83,14 +83,24 @@
|
|||
form (fm/use-form :spec ::login-form :initial initial)
|
||||
|
||||
on-error
|
||||
(fn [_]
|
||||
(reset! error (tr "errors.wrong-credentials")))
|
||||
(fn [cause]
|
||||
(cond
|
||||
(and (= :restriction (:type cause))
|
||||
(= :profile-blocked (:code cause)))
|
||||
(reset! error (tr "errors.profile-blocked"))
|
||||
|
||||
(and (= :validation (:type cause))
|
||||
(= :wrong-credentials (:code cause)))
|
||||
(reset! error (tr "errors.wrong-credentials"))
|
||||
|
||||
:else
|
||||
(reset! error (tr "errors.generic"))))
|
||||
|
||||
on-success-default
|
||||
(fn [data]
|
||||
(when-let [token (:invitation-token data)]
|
||||
(st/emit! (rt/nav :auth-verify-token {} {:token token}))))
|
||||
|
||||
|
||||
on-success
|
||||
(fn [data]
|
||||
(if (nil? on-success-callback)
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::password-1 ::us/not-empty-string)
|
||||
(s/def ::password-2 ::us/not-empty-string)
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::recovery-request-form (s/keys :req-un [::email]))
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc demo-warning
|
||||
[_]
|
||||
|
@ -48,20 +48,23 @@
|
|||
:opt-un [::invitation-token]))
|
||||
|
||||
(defn- handle-prepare-register-error
|
||||
[form error]
|
||||
(case (:code error)
|
||||
:registration-disabled
|
||||
[form {:keys [type code] :as cause}]
|
||||
(condp = [type code]
|
||||
[:restriction :registration-disabled]
|
||||
(st/emit! (dm/error (tr "errors.registration-disabled")))
|
||||
|
||||
:email-has-permanent-bounces
|
||||
[:restriction :profile-blocked]
|
||||
(st/emit! (dm/error (tr "errors.profile-blocked")))
|
||||
|
||||
[:validation :email-has-permanent-bounces]
|
||||
(let [email (get @form [:data :email])]
|
||||
(st/emit! (dm/error (tr "errors.email-has-permanent-bounces" email))))
|
||||
|
||||
:email-already-exists
|
||||
[:validation :email-already-exists]
|
||||
(swap! form assoc-in [:errors :email]
|
||||
{:message "errors.email-already-exists"})
|
||||
|
||||
:email-as-password
|
||||
[:validation :email-as-password]
|
||||
(swap! form assoc-in [:errors :password]
|
||||
{:message "errors.email-as-password"})
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
[app.util.router :as rt]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defmulti handle-token (fn [token] (:iss token)))
|
||||
|
||||
|
@ -62,33 +62,34 @@
|
|||
[{:keys [route] :as props}]
|
||||
(let [token (get-in route [:query-params :token])
|
||||
bad-token (mf/use-state false)]
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(dom/set-html-title (tr "title.default"))
|
||||
(->> (rp/mutation :verify-token {:token token})
|
||||
(rx/subs
|
||||
(fn [tdata]
|
||||
(handle-token tdata))
|
||||
(fn [{:keys [type code] :as error}]
|
||||
(cond
|
||||
(or (= :validation type)
|
||||
(= :invalid-token code)
|
||||
(= :token-expired (:reason error)))
|
||||
(reset! bad-token true)
|
||||
(= :email-already-exists code)
|
||||
(let [msg (tr "errors.email-already-exists")]
|
||||
(ts/schedule 100 #(st/emit! (dm/error msg)))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
|
||||
(= :email-already-validated code)
|
||||
(let [msg (tr "errors.email-already-validated")]
|
||||
(ts/schedule 100 #(st/emit! (dm/warn msg)))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.default"))
|
||||
(->> (rp/command! :verify-token {:token token})
|
||||
(rx/subs
|
||||
(fn [tdata]
|
||||
(handle-token tdata))
|
||||
(fn [{:keys [type code] :as error}]
|
||||
(cond
|
||||
(or (= :validation type)
|
||||
(= :invalid-token code)
|
||||
(= :token-expired (:reason error)))
|
||||
(reset! bad-token true)
|
||||
|
||||
:else
|
||||
(let [msg (tr "errors.generic")]
|
||||
(ts/schedule 100 #(st/emit! (dm/error msg)))
|
||||
(st/emit! (rt/nav :auth-login)))))))))
|
||||
(= :email-already-exists code)
|
||||
(let [msg (tr "errors.email-already-exists")]
|
||||
(ts/schedule 100 #(st/emit! (dm/error msg)))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
|
||||
(= :email-already-validated code)
|
||||
(let [msg (tr "errors.email-already-validated")]
|
||||
(ts/schedule 100 #(st/emit! (dm/warn msg)))
|
||||
(st/emit! (rt/nav :auth-login)))
|
||||
|
||||
:else
|
||||
(let [msg (tr "errors.generic")]
|
||||
(ts/schedule 100 #(st/emit! (dm/error msg)))
|
||||
(st/emit! (rt/nav :auth-login))))))))
|
||||
|
||||
(if @bad-token
|
||||
[:> static/static-header {}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
[app.util.time :as dt]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc resizing-textarea
|
||||
{::mf/wrap-props false}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
(ns app.main.ui.components.code-block
|
||||
(:require
|
||||
["highlight.js" :as hljs]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc code-block [{:keys [code type]}]
|
||||
(let [block-ref (mf/use-ref)]
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
[app.util.color :as uc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn gradient-type->string [type]
|
||||
(case type
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(defn clean-color
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[goog.object :as gobj]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc context-menu
|
||||
{::mf/wrap-props false}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
[app.util.timers :as timers]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc copy-button [{:keys [data on-copied]}]
|
||||
(let [just-copied (mf/use-state false)]
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[goog.events :as events]
|
||||
[goog.object :as gobj]
|
||||
[rumext.alpha :as mf])
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(mf/defc dropdown'
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as timers]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc editable-label
|
||||
[{:keys [value on-change on-cancel editing? disable-dbl-click? class-name] :as props}]
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as timers]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc editable-select
|
||||
[{:keys [value type options class on-change placeholder on-blur] :as params}]
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc file-uploader
|
||||
{::mf/forward-ref true}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
[cljs.core :as c]
|
||||
[clojure.string]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def form-ctx (mf/create-context nil))
|
||||
(def use-form fm/use-form)
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
[app.util.simple-math :as sm]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(mf/defc numeric-input
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
[app.common.uuid :as uuid]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.icons :as i]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc select [{:keys [default-value options class on-change]}]
|
||||
(let [state (mf/use-state {:id (uuid/next)
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
(ns app.main.ui.components.shape-icon
|
||||
(:require
|
||||
[app.main.ui.icons :as i]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
||||
(mf/defc element-icon
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc tab-element
|
||||
{::mf/wrap-props false}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr t]]
|
||||
[app.util.keyboard :as k]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(mf/defc confirm-dialog
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
(ns app.main.ui.context
|
||||
(:require
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def render-id (mf/create-context nil))
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
(:require
|
||||
[app.util.timers :as ts]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Static cursors
|
||||
(def comments (cursor-ref :comments 0 2 20))
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
[goog.events :as events]
|
||||
[okulary.core :as l]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf])
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(defn ^boolean uuid-str?
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::member-id ::us/uuid)
|
||||
(s/def ::leave-modal-form
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc comments-section
|
||||
[{:keys [profile team]}]
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:const options [:all :merge :detach])
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn get-project-name
|
||||
[project]
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc header
|
||||
[{:keys [project on-create-clicked] :as props}]
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
[app.util.keyboard :as kbd]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- use-set-page-title
|
||||
[team section]
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
|
||||
(ns app.main.ui.dashboard.grid
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.refs :as refs]
|
||||
|
@ -19,43 +20,57 @@
|
|||
[app.main.ui.dashboard.import :refer [use-import-file]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.worker :as wrk]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.dom.dnd :as dnd]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.perf :as perf]
|
||||
[app.util.time :as dt]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(log/set-level! :warn)
|
||||
(log/set-level! :info)
|
||||
|
||||
;; --- Grid Item Thumbnail
|
||||
|
||||
(defn ask-for-thumbnail
|
||||
"Creates some hooks to handle the files thumbnails cache"
|
||||
[file]
|
||||
(let [components-v2 (features/active-feature? :components-v2)]
|
||||
(wrk/ask! {:cmd :thumbnails/generate
|
||||
:revn (:revn file)
|
||||
:file-id (:id file)
|
||||
:components-v2 components-v2})))
|
||||
(wrk/ask! {:cmd :thumbnails/generate
|
||||
:revn (:revn file)
|
||||
:file-id (:id file)
|
||||
:file-name (:name file)
|
||||
:components-v2 (features/active-feature? :components-v2)}))
|
||||
|
||||
(mf/defc grid-item-thumbnail
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [file] :as props}]
|
||||
(let [container (mf/use-ref)]
|
||||
(mf/with-effect [file]
|
||||
(->> (ask-for-thumbnail file)
|
||||
(rx/subs (fn [{:keys [data fonts] :as params}]
|
||||
(run! fonts/ensure-loaded! fonts)
|
||||
(when-let [node (mf/ref-val container)]
|
||||
(dom/set-html! node data))))))
|
||||
(let [container (mf/use-ref)
|
||||
bgcolor (dm/get-in file [:data :options :background])
|
||||
visible? (h/use-visible container :once? true)]
|
||||
|
||||
[:div.grid-item-th {:style {:background-color (get-in file [:data :options :background])}
|
||||
:ref container}
|
||||
(mf/with-effect [file visible?]
|
||||
(when visible?
|
||||
(let [tp (perf/tpoint)]
|
||||
(->> (ask-for-thumbnail file)
|
||||
(rx/subscribe-on :af)
|
||||
(rx/subs (fn [{:keys [data fonts] :as params}]
|
||||
(run! fonts/ensure-loaded! fonts)
|
||||
(log/info :hint "loaded thumbnail"
|
||||
:file-id (dm/str (:id file))
|
||||
:file-name (:name file)
|
||||
:elapsed (str/ffmt "%ms" (tp)))
|
||||
(when-let [node (mf/ref-val container)]
|
||||
(dom/set-html! node data))))))))
|
||||
|
||||
[:div.grid-item-th
|
||||
{:style {:background-color bgcolor}
|
||||
:ref container}
|
||||
i/loader-pencil]))
|
||||
|
||||
;; --- Grid Item Library
|
||||
|
@ -144,6 +159,7 @@
|
|||
|
||||
(mf/defc grid-item-metadata
|
||||
[{:keys [modified-at]}]
|
||||
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
time (dt/timeago modified-at {:locale locale})]
|
||||
[:span.date
|
||||
|
@ -159,18 +175,19 @@
|
|||
(mf/defc grid-item
|
||||
{:wrap [mf/memo]}
|
||||
[{:keys [file navigate? origin library-view?] :as props}]
|
||||
(let [file-id (:id file)
|
||||
local (mf/use-state {:menu-open false
|
||||
:menu-pos nil
|
||||
:edition false})
|
||||
selected-files (mf/deref refs/dashboard-selected-files)
|
||||
dashboard-local (mf/deref refs/dashboard-local)
|
||||
item-ref (mf/use-ref)
|
||||
menu-ref (mf/use-ref)
|
||||
selected? (contains? selected-files file-id)
|
||||
(let [file-id (:id file)
|
||||
local (mf/use-state {:menu-open false
|
||||
:menu-pos nil
|
||||
:edition false})
|
||||
selected-files (mf/deref refs/dashboard-selected-files)
|
||||
dashboard-local (mf/deref refs/dashboard-local)
|
||||
node-ref (mf/use-ref)
|
||||
menu-ref (mf/use-ref)
|
||||
|
||||
selected? (contains? selected-files file-id)
|
||||
|
||||
on-menu-close
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
#(swap! local assoc :menu-open false))
|
||||
|
||||
on-select
|
||||
|
@ -184,7 +201,7 @@
|
|||
(st/emit! (dd/toggle-file-select file)))))
|
||||
|
||||
on-navigate
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(let [menu-icon (mf/ref-val menu-ref)
|
||||
|
@ -193,14 +210,14 @@
|
|||
(st/emit! (dd/go-to-workspace file))))))
|
||||
|
||||
on-drag-start
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps selected-files)
|
||||
(fn [event]
|
||||
(let [offset (dom/get-offset-position (.-nativeEvent event))
|
||||
|
||||
select-current? (not (contains? selected-files (:id file)))
|
||||
|
||||
item-el (mf/ref-val item-ref)
|
||||
item-el (mf/ref-val node-ref)
|
||||
counter-el (create-counter-element item-el
|
||||
(if select-current?
|
||||
1
|
||||
|
@ -221,7 +238,7 @@
|
|||
(ts/raf #(.removeChild ^js item-el counter-el)))))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps file selected?)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
|
@ -236,14 +253,14 @@
|
|||
:menu-pos position))))
|
||||
|
||||
edit
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps file)
|
||||
(fn [name]
|
||||
(st/emit! (dd/rename-file (assoc file :name name)))
|
||||
(swap! local assoc :edition false)))
|
||||
|
||||
on-edit
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps file)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
|
@ -251,16 +268,14 @@
|
|||
:edition true
|
||||
:menu-open false)))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps selected? local)
|
||||
(fn []
|
||||
(when (and (not selected?) (:menu-open @local))
|
||||
(swap! local assoc :menu-open false))))
|
||||
(mf/with-effect [selected? local]
|
||||
(when (and (not selected?) (:menu-open @local))
|
||||
(swap! local assoc :menu-open false)))
|
||||
|
||||
[:div.grid-item.project-th
|
||||
{:class (dom/classnames :selected selected?
|
||||
:library library-view?)
|
||||
:ref item-ref
|
||||
:ref node-ref
|
||||
:draggable true
|
||||
:on-click on-select
|
||||
:on-double-click on-navigate
|
||||
|
@ -296,13 +311,15 @@
|
|||
:origin origin
|
||||
:dashboard-local dashboard-local}])]]]))
|
||||
|
||||
|
||||
(mf/defc grid
|
||||
[{:keys [files project on-create-clicked origin limit library-view?] :as props}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
project-id (:id project)
|
||||
node-ref (mf/use-var nil)
|
||||
|
||||
on-finish-import
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (dd/fetch-files {:project-id project-id})
|
||||
(dd/fetch-shared-files)
|
||||
|
@ -311,7 +328,7 @@
|
|||
import-files (use-import-file project-id on-finish-import)
|
||||
|
||||
on-drag-enter
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(when (or (dnd/has-type? e "Files")
|
||||
(dnd/has-type? e "application/x-moz-file"))
|
||||
|
@ -319,32 +336,34 @@
|
|||
(reset! dragging? true))))
|
||||
|
||||
on-drag-over
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(when (or (dnd/has-type? e "Files")
|
||||
(dnd/has-type? e "application/x-moz-file"))
|
||||
(dom/prevent-default e))))
|
||||
|
||||
on-drag-leave
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(when-not (dnd/from-child? e)
|
||||
(reset! dragging? false))))
|
||||
|
||||
|
||||
on-drop
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(when (or (dnd/has-type? e "Files")
|
||||
(dnd/has-type? e "application/x-moz-file"))
|
||||
(dom/prevent-default e)
|
||||
(reset! dragging? false)
|
||||
(import-files (.-files (.-dataTransfer e))))))]
|
||||
(import-files (.-files (.-dataTransfer e))))))
|
||||
]
|
||||
|
||||
[:section.dashboard-grid {:on-drag-enter on-drag-enter
|
||||
:on-drag-over on-drag-over
|
||||
:on-drag-leave on-drag-leave
|
||||
:on-drop on-drop}
|
||||
[:section.dashboard-grid
|
||||
{:on-drag-enter on-drag-enter
|
||||
:on-drag-over on-drag-over
|
||||
:on-drag-leave on-drag-leave
|
||||
:on-drop on-drop
|
||||
:ref node-ref}
|
||||
(cond
|
||||
(nil? files)
|
||||
[:& loading-placeholder]
|
||||
|
@ -352,8 +371,10 @@
|
|||
(seq files)
|
||||
[:div.grid-row
|
||||
{:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}}
|
||||
|
||||
(when @dragging?
|
||||
[:div.grid-item])
|
||||
|
||||
(for [item files]
|
||||
[:& grid-item
|
||||
{:file item
|
||||
|
@ -361,21 +382,21 @@
|
|||
:navigate? true
|
||||
:origin origin
|
||||
:library-view? library-view?}])]
|
||||
|
||||
:else
|
||||
[:& empty-placeholder {:default? (:is-default project)
|
||||
:on-create-clicked on-create-clicked
|
||||
:project project
|
||||
:limit limit
|
||||
:origin origin}])]))
|
||||
[:& empty-placeholder
|
||||
{:default? (:is-default project)
|
||||
:on-create-clicked on-create-clicked
|
||||
:project project
|
||||
:limit limit
|
||||
:origin origin}])]))
|
||||
|
||||
(mf/defc line-grid-row
|
||||
[{:keys [files selected-files dragging? limit] :as props}]
|
||||
(let [limit (if dragging?
|
||||
(dec limit)
|
||||
limit)]
|
||||
|
||||
(let [limit (if dragging? (dec limit) limit)]
|
||||
[:div.grid-row.no-wrap
|
||||
{:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}}
|
||||
{:style {:grid-template-columns (dm/str "repeat(" limit ", 1fr)")}}
|
||||
|
||||
(when dragging?
|
||||
[:div.grid-item])
|
||||
(for [item (take limit files)]
|
||||
|
@ -396,8 +417,8 @@
|
|||
selected-project (mf/deref refs/dashboard-selected-project)
|
||||
|
||||
on-finish-import
|
||||
(mf/use-callback
|
||||
(mf/deps (:id team))
|
||||
(mf/use-fn
|
||||
(mf/deps team-id)
|
||||
(fn []
|
||||
(st/emit! (dd/fetch-recent-files (:id team))
|
||||
(dd/clear-selected-files))))
|
||||
|
@ -405,7 +426,7 @@
|
|||
import-files (use-import-file project-id on-finish-import)
|
||||
|
||||
on-drag-enter
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps selected-project)
|
||||
(fn [e]
|
||||
(when (dnd/has-type? e "penpot/files")
|
||||
|
@ -421,7 +442,7 @@
|
|||
(reset! dragging? true))))
|
||||
|
||||
on-drag-over
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(when (or (dnd/has-type? e "penpot/files")
|
||||
(dnd/has-type? e "Files")
|
||||
|
@ -429,19 +450,19 @@
|
|||
(dom/prevent-default e))))
|
||||
|
||||
on-drag-leave
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(when-not (dnd/from-child? e)
|
||||
(reset! dragging? false))))
|
||||
|
||||
on-drop-success
|
||||
(fn []
|
||||
(st/emit! (dm/success (tr "dashboard.success-move-file"))
|
||||
(st/emit! (msg/success (tr "dashboard.success-move-file"))
|
||||
(dd/fetch-recent-files (:id team))
|
||||
(dd/clear-selected-files)))
|
||||
|
||||
on-drop
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps files selected-files)
|
||||
(fn [e]
|
||||
(when (or (dnd/has-type? e "Files")
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(log/set-level! :debug)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc inline-edition
|
||||
[{:keys [content on-end] :as props}]
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc libraries-page
|
||||
[{:keys [team] :as props}]
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc empty-placeholder
|
||||
[{:keys [dragging? on-create-clicked project limit origin] :as props}]
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::project some?)
|
||||
(s/def ::show? boolean?)
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
(ns app.main.ui.dashboard.projects
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.messages :as msg]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.refs :as refs]
|
||||
|
@ -27,7 +28,7 @@
|
|||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc header
|
||||
{::mf/wrap [mf/memo]}
|
||||
|
@ -43,8 +44,15 @@
|
|||
(mf/defc team-hero
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [team close-banner] :as props}]
|
||||
(let [go-members #(st/emit! (dd/go-to-team-members))
|
||||
invite-member #(st/emit! (modal/show {:type :invite-members :team team :origin :hero}))]
|
||||
(let [go-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
|
||||
|
||||
invite-member
|
||||
(mf/use-fn
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(st/emit! (modal/show {:type :invite-members
|
||||
:team team
|
||||
:origin :hero}))))]
|
||||
[:div.team-hero
|
||||
[:img {:src "images/deco-team-banner.png" :border "0"}]
|
||||
[:div.text
|
||||
|
@ -52,7 +60,9 @@
|
|||
[:div.info
|
||||
[:span (tr "dasboard.team-hero.text")]
|
||||
[:a {:on-click go-members} (tr "dasboard.team-hero.management")]]]
|
||||
[:button.btn-primary.invite {:on-click invite-member} (tr "onboarding.choice.team-up.invite-members")]
|
||||
[:button.btn-primary.invite
|
||||
{:on-click invite-member}
|
||||
(tr "onboarding.choice.team-up.invite-members")]
|
||||
[:button.close {:on-click close-banner}
|
||||
[:span i/close]]]))
|
||||
|
||||
|
@ -61,46 +71,47 @@
|
|||
|
||||
(mf/defc tutorial-project
|
||||
[{:keys [close-tutorial default-project-id] :as props}]
|
||||
(let [state (mf/use-state
|
||||
{:status :waiting
|
||||
:file nil})
|
||||
(let [state (mf/use-state {:status :waiting
|
||||
:file nil})
|
||||
|
||||
template (->> (mf/deref builtin-templates)
|
||||
(filter #(= (:id %) "tutorial-for-beginners"))
|
||||
first)
|
||||
templates (mf/deref builtin-templates)
|
||||
template (d/seek #(= (:id %) "tutorial-for-beginners") templates)
|
||||
|
||||
on-template-cloned-success
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps default-project-id)
|
||||
(fn [response]
|
||||
(swap! state #(assoc % :status :success :file (:first response)))
|
||||
(st/emit! (dd/go-to-workspace {:id (first response) :project-id default-project-id :name "tutorial"})
|
||||
(du/update-profile-props {:viewed-tutorial? true}))))
|
||||
|
||||
on-template-cloned-error
|
||||
(fn []
|
||||
(swap! state #(assoc % :status :waiting))
|
||||
(st/emit!
|
||||
(dm/error (tr "dashboard.libraries-and-templates.import-error"))))
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(swap! state #(assoc % :status :waiting))
|
||||
(st/emit!
|
||||
(msg/error (tr "dashboard.libraries-and-templates.import-error")))))
|
||||
|
||||
download-tutorial
|
||||
(fn []
|
||||
(let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error}
|
||||
params {:project-id default-project-id :template-id (:id template)}]
|
||||
(swap! state #(assoc % :status :importing))
|
||||
(st/emit! (with-meta (dd/clone-template (with-meta params mdata))
|
||||
{::ev/origin "get-started-hero-block"}))))]
|
||||
(mf/use-fn
|
||||
(mf/deps template default-project-id)
|
||||
(fn []
|
||||
(let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error}
|
||||
params {:project-id default-project-id :template-id (:id template)}]
|
||||
(swap! state #(assoc % :status :importing))
|
||||
(st/emit! (with-meta (dd/clone-template (with-meta params mdata))
|
||||
{::ev/origin "get-started-hero-block"})))))]
|
||||
[:div.tutorial
|
||||
[:div.img]
|
||||
[:div.text
|
||||
[:div.title (tr "dasboard.tutorial-hero.title")]
|
||||
[:div.info (tr "dasboard.tutorial-hero.info")]
|
||||
[:button.btn-primary.action {:on-click download-tutorial}
|
||||
[:button.btn-primary.action {:on-click download-tutorial}
|
||||
(case (:status @state)
|
||||
:waiting (tr "dasboard.tutorial-hero.start")
|
||||
:importing [:span.loader i/loader-pencil]
|
||||
:success ""
|
||||
)
|
||||
]]
|
||||
:success "")]]
|
||||
|
||||
[:button.close
|
||||
{:on-click close-tutorial}
|
||||
[:span.icon i/close]]]))
|
||||
|
@ -128,32 +139,32 @@
|
|||
[{:keys [project first? team files] :as props}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
file-count (or (:count project) 0)
|
||||
project-id (:id project)
|
||||
|
||||
dstate (mf/deref refs/dashboard-local)
|
||||
edit-id (:project-for-edit dstate)
|
||||
|
||||
local
|
||||
(mf/use-state {:menu-open false
|
||||
:menu-pos nil
|
||||
:edition? (= (:id project) edit-id)})
|
||||
local (mf/use-state {:menu-open false
|
||||
:menu-pos nil
|
||||
:edition? (= (:id project) edit-id)})
|
||||
|
||||
width (mf/use-state nil)
|
||||
rowref (mf/use-ref)
|
||||
itemsize (if (>= @width 1030)
|
||||
280
|
||||
230)
|
||||
|
||||
ratio (if (some? @width) (/ @width itemsize) 0)
|
||||
nitems (mth/floor ratio)
|
||||
limit (min 10 nitems)
|
||||
limit (max 1 limit)
|
||||
|
||||
on-nav
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(mf/deps project)
|
||||
#(st/emit! (rt/nav :dashboard-files {:team-id (:team-id project)
|
||||
:project-id (:id project)})))
|
||||
|
||||
width (mf/use-state nil)
|
||||
rowref (mf/use-ref)
|
||||
itemsize (if (>= @width 1030)
|
||||
280
|
||||
230)
|
||||
|
||||
ratio (if (some? @width) (/ @width itemsize) 0)
|
||||
nitems (mth/floor ratio)
|
||||
limit (min 10 nitems)
|
||||
limit (max 1 limit)
|
||||
|
||||
(fn []
|
||||
(st/emit! (rt/nav :dashboard-files {:team-id (:team-id project)
|
||||
:project-id project-id}))))
|
||||
toggle-pin
|
||||
(mf/use-callback
|
||||
(mf/deps project)
|
||||
|
@ -209,22 +220,24 @@
|
|||
(dd/fetch-recent-files (:id team))
|
||||
(dd/clear-selected-files))))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [node (mf/ref-val rowref)
|
||||
mnt? (volatile! true)
|
||||
sub (->> (wapi/observe-resize node)
|
||||
(rx/observe-on :af)
|
||||
(rx/subs (fn [entries]
|
||||
(let [row (first entries)
|
||||
row-rect (.-contentRect ^js row)
|
||||
row-width (.-width ^js row-rect)]
|
||||
(when @mnt?
|
||||
(reset! width row-width))))))]
|
||||
(fn []
|
||||
(vreset! mnt? false)
|
||||
(rx/dispose! sub)))))
|
||||
[:div.dashboard-project-row {:class (when first? "first")}
|
||||
(mf/with-effect
|
||||
(let [node (mf/ref-val rowref)
|
||||
mnt? (volatile! true)
|
||||
sub (->> (wapi/observe-resize node)
|
||||
(rx/observe-on :af)
|
||||
(rx/subs (fn [entries]
|
||||
(let [row (first entries)
|
||||
row-rect (.-contentRect ^js row)
|
||||
row-width (.-width ^js row-rect)]
|
||||
(when @mnt?
|
||||
(reset! width row-width))))))]
|
||||
(fn []
|
||||
(vreset! mnt? false)
|
||||
(rx/dispose! sub))))
|
||||
|
||||
|
||||
[:div.dashboard-project-row
|
||||
{:class (when first? "first")}
|
||||
[:div.project {:ref rowref}
|
||||
[:div.project-name-wrapper
|
||||
(if (:edition? @local)
|
||||
|
@ -265,6 +278,7 @@
|
|||
[:a.btn-secondary.btn-small.tooltip.tooltip-bottom
|
||||
{:on-click on-menu-click :alt (tr "dashboard.options") :data-test "project-options"}
|
||||
i/actions]]]
|
||||
|
||||
(when (and (> limit 0)
|
||||
(> file-count limit))
|
||||
[:div.show-more {:on-click on-nav}
|
||||
|
@ -290,53 +304,53 @@
|
|||
(reverse))
|
||||
recent-map (mf/deref recent-files-ref)
|
||||
props (some-> profile (get :props {}))
|
||||
team-hero? (:team-hero? props true)
|
||||
team-hero? (and (:team-hero? props true)
|
||||
(not (:is-default team)))
|
||||
tutorial-viewed? (:viewed-tutorial? props true)
|
||||
walkthrough-viewed? (:viewed-walkthrough? props true)
|
||||
|
||||
close-banner (fn []
|
||||
(st/emit!
|
||||
(du/update-profile-props {:team-hero? false})
|
||||
(ptk/event ::ev/event {::ev/name "dont-show-team-up-hero"
|
||||
::ev/origin "dashboard"})))
|
||||
team-id (:id team)
|
||||
|
||||
close-tutorial (fn []
|
||||
(st/emit!
|
||||
(du/update-profile-props {:viewed-tutorial? true})
|
||||
(ptk/event ::ev/event {::ev/name "dont-show"
|
||||
::ev/origin "get-started-hero-block"
|
||||
:type "tutorial"
|
||||
:section "dashboard"})))
|
||||
close-banner
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/update-profile-props {:team-hero? false})
|
||||
(ptk/event ::ev/event {::ev/name "dont-show-team-up-hero"
|
||||
::ev/origin "dashboard"}))))
|
||||
close-tutorial
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/update-profile-props {:viewed-tutorial? true})
|
||||
(ptk/event ::ev/event {::ev/name "dont-show"
|
||||
::ev/origin "get-started-hero-block"
|
||||
:type "tutorial"
|
||||
:section "dashboard"}))))
|
||||
close-walkthrough
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (du/update-profile-props {:viewed-walkthrough? true})
|
||||
(ptk/event ::ev/event {::ev/name "dont-show"
|
||||
::ev/origin "get-started-hero-block"
|
||||
:type "walkthrough"
|
||||
:section "dashboard"}))))]
|
||||
|
||||
close-walkthrough (fn []
|
||||
(st/emit!
|
||||
(du/update-profile-props {:viewed-walkthrough? true})
|
||||
(ptk/event ::ev/event {::ev/name "dont-show"
|
||||
::ev/origin "get-started-hero-block"
|
||||
:type "walkthrough"
|
||||
:section "dashboard"})))]
|
||||
(mf/with-effect [team]
|
||||
(let [tname (if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team))]
|
||||
(dom/set-html-title (tr "title.dashboard.projects" tname))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(let [tname (if (:is-default team)
|
||||
(tr "dashboard.your-penpot")
|
||||
(:name team))]
|
||||
(dom/set-html-title (tr "title.dashboard.projects" tname)))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps (:id team))
|
||||
(fn []
|
||||
(st/emit! (dd/fetch-recent-files (:id team))
|
||||
(dd/clear-selected-files))))
|
||||
(mf/with-effect [team-id]
|
||||
(st/emit! (dd/fetch-recent-files team-id)
|
||||
(dd/clear-selected-files)))
|
||||
|
||||
(when (seq projects)
|
||||
[:*
|
||||
[:& header]
|
||||
(when (and team-hero? (not (:is-default team)))
|
||||
[:& team-hero
|
||||
{:team team
|
||||
:close-banner close-banner}])
|
||||
|
||||
(when team-hero?
|
||||
[:& team-hero {:team team :close-banner close-banner}])
|
||||
|
||||
(when (or (not tutorial-viewed?) (not walkthrough-viewed?))
|
||||
[:div.hero-projects
|
||||
(when (and (not tutorial-viewed?) (:is-default team))
|
||||
|
@ -358,5 +372,5 @@
|
|||
:team team
|
||||
:files files
|
||||
:first? (= project (first projects))
|
||||
:key (:id project)}]))]])))
|
||||
:key id}]))]])))
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc search-page
|
||||
[{:keys [team search-term] :as props}]
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
[cljs.spec.alpha :as s]
|
||||
[goog.functions :as f]
|
||||
[potok.core :as ptk]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc sidebar-project
|
||||
[{:keys [item selected?] :as props}]
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc header
|
||||
{::mf/wrap [mf/memo]}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
[app.util.router :as rt]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::team-form
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[goog.events :as events]
|
||||
[rumext.alpha :as mf])
|
||||
[rumext.v2 :as mf])
|
||||
(:import goog.events.EventType))
|
||||
|
||||
(mf/defc delete-shared-dialog
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
[app.util.i18n :as i18n :refer [tr c]]
|
||||
[app.util.strings :as ust]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc export-multiple-dialog
|
||||
[{:keys [exports title cmd no-selection]}]
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
[goog.functions :as f]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn use-id
|
||||
"Get a stable id value across rerenders."
|
||||
|
@ -29,7 +29,7 @@
|
|||
|
||||
(defn use-rxsub
|
||||
[ob]
|
||||
(let [[state reset-state!] (mf/useState @ob)]
|
||||
(let [[state reset-state!] (mf/useState #(if (satisfies? IDeref ob) @ob nil))]
|
||||
(mf/useEffect
|
||||
(fn []
|
||||
(let [sub (rx/subscribe ob #(reset-state! %))]
|
||||
|
@ -313,3 +313,39 @@
|
|||
(use-stream stream (partial reset! state))
|
||||
|
||||
state))
|
||||
|
||||
(defonce ^:private intersection-subject (rx/subject))
|
||||
(defonce ^:private intersection-observer
|
||||
(delay (js/IntersectionObserver.
|
||||
(fn [entries _]
|
||||
(run! (partial rx/push! intersection-subject) (seq entries)))
|
||||
#js {:rootMargin "0px"
|
||||
:threshold 1.0})))
|
||||
|
||||
(defn use-visible
|
||||
[ref & {:keys [once?]}]
|
||||
(let [[state update-state!] (mf/useState false)]
|
||||
(mf/with-effect [once?]
|
||||
(let [node (mf/ref-val ref)
|
||||
stream (->> intersection-subject
|
||||
(rx/filter (fn [entry]
|
||||
(let [target (unchecked-get entry "target")]
|
||||
(identical? target node))))
|
||||
(rx/map (fn [entry]
|
||||
(let [ratio (unchecked-get entry "intersectionRatio")
|
||||
intersecting? (unchecked-get entry "isIntersecting")]
|
||||
(or intersecting? (> ratio 0.5)))))
|
||||
(rx/dedupe))
|
||||
stream (if once?
|
||||
(->> stream
|
||||
(rx/filter identity)
|
||||
(rx/take 1))
|
||||
stream)
|
||||
subs (rx/subscribe stream update-state!)]
|
||||
(.observe ^js @intersection-observer node)
|
||||
(fn []
|
||||
(.unobserve ^js @intersection-observer node)
|
||||
(rx/dispose! subs))))
|
||||
|
||||
state))
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as log]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(log/set-level! :warn)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
[app.main.ui.hooks :as hooks]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.storage :refer [storage]]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(log/set-level! :warn)
|
||||
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.icons
|
||||
(:require [rumext.alpha]))
|
||||
(:require [rumext.v2]))
|
||||
|
||||
(defmacro icon-xref
|
||||
[id]
|
||||
(let [href (str "#icon-" (name id))
|
||||
class (str "icon-" (name id))]
|
||||
`(rumext.alpha/html
|
||||
`(rumext.v2/html
|
||||
[:svg {:width 500 :height 500 :class ~class}
|
||||
[:use {:href ~href}]])))
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
(ns app.main.ui.icons
|
||||
(:refer-clojure :exclude [import mask])
|
||||
(:require-macros [app.main.ui.icons :refer [icon-xref]])
|
||||
(:require [rumext.alpha :as mf]))
|
||||
(:require [rumext.v2 :as mf]))
|
||||
|
||||
;; Keep the list of icons sorted
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
(:require
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Component
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
[app.common.math :as mth]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; ------------------------------------------------
|
||||
;; CONSTANTS
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[rumext.alpha :as mf]))
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc banner
|
||||
[{:keys [type position status controls content actions on-close data-test] :as props}]
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue